mirror of
https://github.com/2dust/v2rayNG.git
synced 2026-10-30 06:56:25 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5436d67e47 | ||
|
|
1c3d247071 | ||
|
|
d3e1e7c6fe | ||
|
|
a65c57ea0d | ||
|
|
8586c2550f | ||
|
|
fa6c4807f3 | ||
|
|
6e65f46e3f | ||
|
|
98e7b8e1a1 | ||
|
|
7dd42528db | ||
|
|
e580b76e9e | ||
|
|
c4d02c00ce | ||
|
|
007a4e674f | ||
|
|
46af9d803c | ||
|
|
32a9263e0a | ||
|
|
253e171fec | ||
|
|
31b66a705b | ||
|
|
23c7cd5b95 | ||
|
|
32a37dd626 | ||
|
|
53e84d5fd8 | ||
|
|
0519d02001 | ||
|
|
6dfed5bbc2 | ||
|
|
dcd060e350 | ||
|
|
71f94db929 | ||
|
|
d06b9cb999 | ||
|
|
f53a91241c | ||
|
|
3fb067dc99 | ||
|
|
02103287e5 |
No files matched your search
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: '0'
|
||||
|
||||
@@ -11,6 +11,6 @@ jobs:
|
||||
go:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Validate Fastlane Supply Metadata
|
||||
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.1.0
|
||||
+1
-1
Submodule AndroidLibXrayLite updated: e194d22cf1...85f272f37c.
@@ -3,7 +3,7 @@
|
||||
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
||||
|
||||
[](https://developer.android.com/about/versions/lollipop)
|
||||
[](https://kotlinlang.org)
|
||||
[](https://kotlinlang.org)
|
||||
[](https://github.com/2dust/v2rayNG/commits/master)
|
||||
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
||||
[](https://github.com/2dust/v2rayNG/releases)
|
||||
|
||||
@@ -12,8 +12,8 @@ android {
|
||||
applicationId = "com.v2ray.ang"
|
||||
minSdk = 24
|
||||
targetSdk = 37
|
||||
versionCode = 733
|
||||
versionName = "2.2.3"
|
||||
versionCode = 735
|
||||
versionName = "2.2.5"
|
||||
multiDexEnabled = true
|
||||
|
||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||
@@ -21,7 +21,7 @@ android {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
if (abiFilterList != null && abiFilterList.isNotEmpty()) {
|
||||
if (!abiFilterList.isNullOrEmpty()) {
|
||||
include(*abiFilterList.toTypedArray())
|
||||
} else {
|
||||
include(
|
||||
|
||||
@@ -203,13 +203,19 @@ object AppConfig {
|
||||
const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com"
|
||||
|
||||
// Android Private DNS constants
|
||||
const val DNS_DNSPOD_DOMAIN = "dot.pub"
|
||||
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
|
||||
const val DNS_CISCO_SSE_DOMAIN = "dns.sse.cisco.com"
|
||||
const val DNS_CISCO_UMBRELLA_DOMAIN = "dns.umbrella.com"
|
||||
const val DNS_CLOUDFLARE_ONE_DOMAIN = "one.one.one.one"
|
||||
const val DNS_CLOUDFLARE_ONEDOT_DNS_DOMAIN = "1dot1dot1dot1.cloudflare-dns.com"
|
||||
const val DNS_CLOUDFLARE_DNS_COM_DOMAIN = "dns.cloudflare.com"
|
||||
const val DNS_CLOUDFLARE_DNS_DOMAIN = "cloudflare-dns.com"
|
||||
const val DNS_CLOUDFLARE_WARP_DOMAIN = "engage.cloudflareclient.com"
|
||||
const val DNS_DNSPOD_DOH_DOMAIN = "doh.pub"
|
||||
const val DNS_DNSPOD_DOT_DOMAIN = "dot.pub"
|
||||
const val DNS_GOOGLE_DOMAIN = "dns.google"
|
||||
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
|
||||
const val DNS_SB_DOMAIN = "dns.sb"
|
||||
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
|
||||
|
||||
const val DEFAULT_PORT = 443
|
||||
@@ -223,12 +229,18 @@ object AppConfig {
|
||||
const val UNIDENTIFIED_PACKAGE = "__unknown_app__"
|
||||
|
||||
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
|
||||
val DNS_CISCO_SSE_ADDRESSES = arrayListOf("208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53")
|
||||
val DNS_CISCO_UMBRELLA_ADDRESSES = arrayListOf("208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53")
|
||||
val DNS_CLOUDFLARE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
||||
val DNS_CLOUDFLARE_DNS_COM_ADDRESSES = arrayListOf("104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5")
|
||||
val DNS_CLOUDFLARE_ONEDOT_DNS_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
||||
val DNS_CLOUDFLARE_DNS_COM_ADDRESSES = arrayListOf("162.159.61.8", "172.64.41.8", "2a06:98c1:52::8", "2803:f800:53::8")
|
||||
val DNS_CLOUDFLARE_DNS_ADDRESSES = arrayListOf("104.16.248.249", "104.16.249.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9")
|
||||
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
||||
val DNS_CLOUDFLARE_WARP_ADDRESSES = arrayListOf("162.159.192.1", "2606:4700:d0::a29f:c001")
|
||||
val DNS_DNSPOD_DOH_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
||||
val DNS_DNSPOD_DOT_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
||||
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
|
||||
val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
|
||||
val DNS_SB_ADDRESSES = arrayListOf("45.11.45.11", "185.222.222.222", "2a09::", "2a11::")
|
||||
val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
|
||||
|
||||
//minimum list https://serverfault.com/a/304791
|
||||
|
||||
@@ -710,12 +710,18 @@ object CoreConfigManager {
|
||||
|
||||
// hardcode popular Android Private DNS rule to fix localhost DNS problem
|
||||
hosts[AppConfig.DNS_ALIDNS_DOMAIN] = AppConfig.DNS_ALIDNS_ADDRESSES
|
||||
hosts[AppConfig.DNS_CISCO_SSE_DOMAIN] = AppConfig.DNS_CISCO_SSE_ADDRESSES
|
||||
hosts[AppConfig.DNS_CISCO_UMBRELLA_DOMAIN] = AppConfig.DNS_CISCO_UMBRELLA_ADDRESSES
|
||||
hosts[AppConfig.DNS_CLOUDFLARE_ONE_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ONE_ADDRESSES
|
||||
hosts[AppConfig.DNS_CLOUDFLARE_ONEDOT_DNS_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ONEDOT_DNS_ADDRESSES
|
||||
hosts[AppConfig.DNS_CLOUDFLARE_DNS_COM_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_COM_ADDRESSES
|
||||
hosts[AppConfig.DNS_CLOUDFLARE_DNS_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_ADDRESSES
|
||||
hosts[AppConfig.DNS_DNSPOD_DOMAIN] = AppConfig.DNS_DNSPOD_ADDRESSES
|
||||
hosts[AppConfig.DNS_CLOUDFLARE_WARP_DOMAIN] = AppConfig.DNS_CLOUDFLARE_WARP_ADDRESSES
|
||||
hosts[AppConfig.DNS_DNSPOD_DOH_DOMAIN] = AppConfig.DNS_DNSPOD_DOH_ADDRESSES
|
||||
hosts[AppConfig.DNS_DNSPOD_DOT_DOMAIN] = AppConfig.DNS_DNSPOD_DOT_ADDRESSES
|
||||
hosts[AppConfig.DNS_GOOGLE_DOMAIN] = AppConfig.DNS_GOOGLE_ADDRESSES
|
||||
hosts[AppConfig.DNS_QUAD9_DOMAIN] = AppConfig.DNS_QUAD9_ADDRESSES
|
||||
hosts[AppConfig.DNS_SB_DOMAIN] = AppConfig.DNS_SB_ADDRESSES
|
||||
hosts[AppConfig.DNS_YANDEX_DOMAIN] = AppConfig.DNS_YANDEX_ADDRESSES
|
||||
|
||||
//User DNS hosts
|
||||
|
||||
@@ -368,37 +368,40 @@ object CoreOutboundBuilder {
|
||||
profileItem.kcpMtu?.let { kcpSetting.mtu = it }
|
||||
profileItem.kcpTti?.let { kcpSetting.tti = it }
|
||||
streamSettings.kcpSettings = kcpSetting
|
||||
val udpMaskList = mutableListOf<OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean>()
|
||||
val udpMaskList =
|
||||
mutableListOf<OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean>()
|
||||
if (!headerType.isNullOrEmpty() && headerType != "none") {
|
||||
val kcpHeaderType = when {
|
||||
headerType == "wechat-video" -> "header-wechat"
|
||||
else -> "header-$headerType"
|
||||
headerType == "wechat-video" -> "wechat"
|
||||
else -> headerType
|
||||
}
|
||||
udpMaskList.add(
|
||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = kcpHeaderType,
|
||||
settings = if (headerType == "dns" && !host.isNullOrEmpty()) {
|
||||
type = "mkcp-legacy",
|
||||
settings =
|
||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||
domain = host
|
||||
header = kcpHeaderType,
|
||||
value = if (headerType == "dns" && !host.isNullOrEmpty()) {
|
||||
host
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
if (seed.isNullOrEmpty()) {
|
||||
udpMaskList.add(
|
||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = "mkcp-original"
|
||||
type = "mkcp-legacy"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
udpMaskList.add(
|
||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
type = "mkcp-aes128gcm",
|
||||
type = "mkcp-legacy",
|
||||
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||
password = seed
|
||||
value = seed
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -564,6 +567,7 @@ object CoreOutboundBuilder {
|
||||
fingerprint = profileItem.fingerPrint.nullIfBlank(),
|
||||
alpn = profileItem.alpn?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }.takeIf { !it.isNullOrEmpty() },
|
||||
echConfigList = profileItem.echConfigList.nullIfBlank(),
|
||||
verifyPeerCertByName = profileItem.verifyPeerCertByName.nullIfBlank(),
|
||||
pinnedPeerCertSha256 = profileItem.pinnedCA256.nullIfBlank(),
|
||||
publicKey = profileItem.publicKey.nullIfBlank(),
|
||||
shortId = profileItem.shortId.nullIfBlank(),
|
||||
@@ -611,10 +615,6 @@ object CoreOutboundBuilder {
|
||||
&& packets == "tlshello"
|
||||
) {
|
||||
packets = "1-3"
|
||||
} else if (streamSettings.security == AppConfig.TLS
|
||||
&& packets != "tlshello"
|
||||
) {
|
||||
packets = "tlshello"
|
||||
}
|
||||
|
||||
val fragmentMask = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
data class CertSha256Request(
|
||||
val address: String = "",
|
||||
val port: Int = 443,
|
||||
val serverName: String? = null,
|
||||
val timeoutMs: Long = 5000L,
|
||||
)
|
||||
|
||||
data class CertSha256Result(
|
||||
val sha256: String = "",
|
||||
val error: String = "",
|
||||
)
|
||||
@@ -253,9 +253,9 @@ data class V2rayConfig(
|
||||
val disableSystemRoot: Boolean? = null,
|
||||
val enableSessionResumption: Boolean? = null,
|
||||
var echConfigList: String? = null,
|
||||
var verifyPeerCertByName: String? = null,
|
||||
var pinnedPeerCertSha256: String? = null,
|
||||
// REALITY settings
|
||||
val show: Boolean = false,
|
||||
var publicKey: String? = null,
|
||||
var shortId: String? = null,
|
||||
var spiderX: String? = null,
|
||||
@@ -295,7 +295,8 @@ data class V2rayConfig(
|
||||
) {
|
||||
data class MaskSettingsBean(
|
||||
val password: String? = null,
|
||||
val domain: String? = null,
|
||||
val header: String? = null,
|
||||
val value: String? = null,
|
||||
// fragment
|
||||
val packets: String? = null,
|
||||
val length: String? = null,
|
||||
|
||||
@@ -16,5 +16,7 @@ data class VmessQRCode(
|
||||
var sni: String = "",
|
||||
var alpn: String = "",
|
||||
var fp: String = "",
|
||||
var insecure: String = ""
|
||||
var insecure: String = "",
|
||||
var vcn: String = "",
|
||||
var pcs: String = ""
|
||||
)
|
||||
@@ -42,6 +42,7 @@ data class ProfileItem(
|
||||
var fingerPrint: String? = null,
|
||||
var insecure: Boolean? = null,
|
||||
var echConfigList: String? = null,
|
||||
var verifyPeerCertByName: String? = null,
|
||||
var pinnedCA256: String? = null,
|
||||
|
||||
var publicKey: String? = null,
|
||||
|
||||
@@ -84,6 +84,7 @@ open class FmtBase {
|
||||
config.fingerPrint = queryParam["fp"]
|
||||
config.alpn = queryParam["alpn"]
|
||||
config.echConfigList = queryParam["ech"]
|
||||
config.verifyPeerCertByName = queryParam["vcn"]
|
||||
config.pinnedCA256 = queryParam["pcs"]
|
||||
config.publicKey = queryParam["pbk"]
|
||||
config.shortId = queryParam["sid"]
|
||||
@@ -104,6 +105,7 @@ open class FmtBase {
|
||||
config.sni?.nullIfBlank()?.let { dicQuery["sni"] = it }
|
||||
config.alpn?.nullIfBlank()?.let { dicQuery["alpn"] = it }
|
||||
config.echConfigList?.nullIfBlank()?.let { dicQuery["ech"] = it }
|
||||
config.verifyPeerCertByName?.nullIfBlank()?.let { dicQuery["vcn"] = it }
|
||||
config.pinnedCA256?.nullIfBlank()?.let { dicQuery["pcs"] = it }
|
||||
config.fingerPrint?.nullIfBlank()?.let { dicQuery["fp"] = it }
|
||||
config.publicKey?.nullIfBlank()?.let { dicQuery["pbk"] = it }
|
||||
|
||||
@@ -88,6 +88,9 @@ object VmessFmt : FmtBase() {
|
||||
"0" -> false
|
||||
else -> false
|
||||
}
|
||||
config.verifyPeerCertByName = vmessQRCode.vcn
|
||||
config.pinnedCA256 = vmessQRCode.pcs
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -141,6 +144,8 @@ object VmessFmt : FmtBase() {
|
||||
false -> "0"
|
||||
else -> ""
|
||||
}
|
||||
vmessQRCode.vcn = config.verifyPeerCertByName.orEmpty()
|
||||
vmessQRCode.pcs = config.pinnedCA256.orEmpty()
|
||||
|
||||
val json = JsonUtil.toJson(vmessQRCode)
|
||||
return Utils.encode(json)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.v2ray.ang.handler
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.CertSha256Request
|
||||
import com.v2ray.ang.dto.CertSha256Result
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import libv2ray.Libv2ray
|
||||
|
||||
object CertificateFingerprintManager {
|
||||
private const val TIMEOUT_MS = 5000L
|
||||
|
||||
fun fetchForManualFill(profile: ProfileItem): String? {
|
||||
val request = buildRequest(profile) ?: return null
|
||||
val result = if (profile.configType == EConfigType.HYSTERIA2) {
|
||||
fetch("quic", request) { Libv2ray.fetchQuicCertSha256(it) }
|
||||
} else {
|
||||
fetch("tls", request) { Libv2ray.fetchTlsCertSha256(it) }
|
||||
}
|
||||
|
||||
return result
|
||||
?.takeIf { it.error.isBlank() }
|
||||
?.sha256
|
||||
?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
||||
private fun buildRequest(profile: ProfileItem): CertSha256Request? {
|
||||
if (!isFetchable(profile)) return null
|
||||
|
||||
val server = profile.server?.trim()?.takeIf { it.isNotEmpty() } ?: return null
|
||||
val port = profile.serverPort?.toIntOrNull()?.takeIf { it > 0 } ?: AppConfig.DEFAULT_PORT
|
||||
|
||||
return CertSha256Request(
|
||||
address = resolveDialAddress(server),
|
||||
port = port,
|
||||
serverName = inferServerName(profile),
|
||||
timeoutMs = TIMEOUT_MS,
|
||||
)
|
||||
}
|
||||
|
||||
private fun isFetchable(profile: ProfileItem): Boolean {
|
||||
return profile.configType == EConfigType.HYSTERIA2 || profile.security == AppConfig.TLS
|
||||
}
|
||||
|
||||
private fun fetch(
|
||||
type: String,
|
||||
request: CertSha256Request,
|
||||
fetcher: (String) -> String,
|
||||
): CertSha256Result? {
|
||||
return try {
|
||||
JsonUtil.fromJsonSafe(fetcher(JsonUtil.toJson(request)), CertSha256Result::class.java)
|
||||
} catch (e: UnsatisfiedLinkError) {
|
||||
LogUtil.e(AppConfig.TAG, "Fetch $type cert SHA-256 API missing in libv2ray", e)
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Fetch $type cert SHA-256 failed", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveDialAddress(server: String): String {
|
||||
if (Utils.isPureIpAddress(server) || !Utils.isDomainName(server)) return server
|
||||
|
||||
val preferIpv6 = MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6, false)
|
||||
return HttpUtil.resolveHostToIP(server, preferIpv6)
|
||||
?.firstOrNull()
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: server
|
||||
}
|
||||
|
||||
private fun inferServerName(profile: ProfileItem): String? {
|
||||
val sni = profile.sni?.takeIf { it.isNotBlank() }
|
||||
return sni?.takeUnless { Utils.isPureIpAddress(it) }
|
||||
}
|
||||
}
|
||||
@@ -286,9 +286,13 @@ object MmkvManager {
|
||||
*/
|
||||
fun removeAllServer(): Int {
|
||||
val count = profileFullStorage.allKeys()?.count() ?: 0
|
||||
mainStorage.clearAll()
|
||||
profileFullStorage.clearAll()
|
||||
serverAffStorage.clearAll()
|
||||
serverRawStorage.clearAll()
|
||||
|
||||
decodeSubscriptions().forEach { sub ->
|
||||
encodeServerList(mutableListOf(), sub.guid)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ object NotificationManager {
|
||||
stopV2RayPendingIntent
|
||||
)
|
||||
.addAction(
|
||||
R.drawable.ic_delete_24dp,
|
||||
R.drawable.ic_restore_24dp,
|
||||
service.getString(R.string.title_service_restart),
|
||||
restartV2RayPendingIntent
|
||||
)
|
||||
|
||||
@@ -6,8 +6,6 @@ import com.v2ray.ang.dto.UrlContentRequest
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.isActive
|
||||
import java.io.IOException
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
@@ -15,29 +13,6 @@ import java.net.UnknownHostException
|
||||
|
||||
object SpeedtestManager {
|
||||
|
||||
private val tcpTestingSockets = ArrayList<Socket?>()
|
||||
|
||||
/**
|
||||
* Measures the TCP connection time to a given URL and port.
|
||||
*
|
||||
* @param url The URL to connect to.
|
||||
* @param port The port to connect to.
|
||||
* @return The connection time in milliseconds, or -1 if the connection failed.
|
||||
*/
|
||||
suspend fun tcping(url: String, port: Int): Long {
|
||||
var time = -1L
|
||||
for (k in 0 until 2) {
|
||||
val one = socketConnectTime(url, port)
|
||||
if (!currentCoroutineContext().isActive) {
|
||||
break
|
||||
}
|
||||
if (one != -1L && (time == -1L || one < time)) {
|
||||
time = one
|
||||
}
|
||||
}
|
||||
return time
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures the time taken to establish a TCP connection to a given URL and port.
|
||||
*
|
||||
@@ -45,42 +20,34 @@ object SpeedtestManager {
|
||||
* @param port The port to connect to.
|
||||
* @return The connection time in milliseconds, or -1 if the connection failed.
|
||||
*/
|
||||
fun socketConnectTime(url: String, port: Int): Long {
|
||||
fun socketConnectTime(url: String, port: Int, timeoutMs: Int = 1500): Long {
|
||||
var socket: Socket? = null
|
||||
val start = System.currentTimeMillis()
|
||||
|
||||
try {
|
||||
val socket = Socket()
|
||||
synchronized(this) {
|
||||
tcpTestingSockets.add(socket)
|
||||
}
|
||||
val start = System.currentTimeMillis()
|
||||
socket.connect(InetSocketAddress(url, port), 3000)
|
||||
val time = System.currentTimeMillis() - start
|
||||
synchronized(this) {
|
||||
tcpTestingSockets.remove(socket)
|
||||
}
|
||||
socket.close()
|
||||
return time
|
||||
socket = Socket()
|
||||
socket.connect(InetSocketAddress(url, port), timeoutMs)
|
||||
|
||||
return System.currentTimeMillis() - start
|
||||
} catch (e: UnknownHostException) {
|
||||
LogUtil.e(AppConfig.TAG, "Unknown host: $url", e)
|
||||
} catch (e: IOException) {
|
||||
LogUtil.e(AppConfig.TAG, "socketConnectTime IOException: $e")
|
||||
LogUtil.e(AppConfig.TAG, "socketConnectTime IOException: ${e.message}")
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to establish socket connection to $url:$port", e)
|
||||
} finally {
|
||||
socket?.let { s ->
|
||||
try {
|
||||
if (!s.isClosed) {
|
||||
s.close()
|
||||
}
|
||||
} catch (closeEx: IOException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all TCP sockets that are currently being tested.
|
||||
*/
|
||||
fun closeAllTcpSockets() {
|
||||
synchronized(this) {
|
||||
tcpTestingSockets.forEach {
|
||||
it?.close()
|
||||
}
|
||||
tcpTestingSockets.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun getRemoteIPInfo(): String? {
|
||||
val url = MmkvManager.decodeSettingsString(AppConfig.PREF_IP_API_URL)
|
||||
.takeIf { !it.isNullOrBlank() } ?: AppConfig.IP_API_URL
|
||||
|
||||
@@ -4,7 +4,12 @@ import android.content.Context
|
||||
import com.v2ray.ang.core.CoreConfigManager
|
||||
import com.v2ray.ang.core.CoreNativeManager
|
||||
import com.v2ray.ang.dto.RealPingEvent
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.isComplexType
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.handler.SpeedtestManager
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -76,6 +81,21 @@ class RealPingWorkerService(
|
||||
|
||||
private fun startRealPing(guid: String): Long {
|
||||
val retFailure = -1L
|
||||
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: return retFailure
|
||||
if (!config.configType.isComplexType()
|
||||
&& config.configType != EConfigType.HYSTERIA2
|
||||
&& config.server.isNotNullEmpty()
|
||||
&& config.serverPort?.toIntOrNull() != null
|
||||
) {
|
||||
val url = config.server.orEmpty()
|
||||
val port = config.serverPort.orEmpty().toInt()
|
||||
val tcpTime = SpeedtestManager.socketConnectTime(url, port, 1000)
|
||||
if (tcpTime <= -1L) {
|
||||
return retFailure
|
||||
}
|
||||
}
|
||||
|
||||
val configResult = CoreConfigManager.getV2rayConfig4Speedtest(context, guid)
|
||||
if (!configResult.status) {
|
||||
return retFailure
|
||||
|
||||
@@ -226,6 +226,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
||||
private fun removeServerSub(guid: String, position: Int) {
|
||||
mainViewModel.removeServer(guid)
|
||||
adapter.removeServerSub(guid, position)
|
||||
ownerActivity.refreshGroupTabTitles()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -76,23 +76,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
binding.viewPager.isUserInputEnabled = true
|
||||
|
||||
// setup navigation drawer
|
||||
val toggle = ActionBarDrawerToggle(
|
||||
this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close
|
||||
)
|
||||
binding.drawerLayout.addDrawerListener(toggle)
|
||||
toggle.syncState()
|
||||
binding.navView.setNavigationItemSelectedListener(this)
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
} else {
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
})
|
||||
setupNavigationDrawer()
|
||||
|
||||
binding.fab.setOnClickListener { handleFabAction() }
|
||||
binding.layoutTest.setOnClickListener { handleLayoutTestClick() }
|
||||
@@ -106,6 +90,31 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupNavigationDrawer() {
|
||||
val toggle = ActionBarDrawerToggle(
|
||||
this,
|
||||
binding.drawerLayout,
|
||||
binding.toolbar,
|
||||
R.string.navigation_drawer_open,
|
||||
R.string.navigation_drawer_close
|
||||
)
|
||||
binding.drawerLayout.addDrawerListener(toggle)
|
||||
toggle.syncState()
|
||||
binding.navView.setNavigationItemSelectedListener(this)
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
} else {
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupViewModel() {
|
||||
mainViewModel.updateTestResultAction.observe(this) { setTestState(it) }
|
||||
mainViewModel.isRunning.observe(this) { isRunning ->
|
||||
@@ -131,6 +140,26 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
binding.viewPager.setCurrentItem(targetIndex, false)
|
||||
|
||||
binding.tabGroup.isVisible = groups.size > 1
|
||||
refreshGroupTabTitles(true)
|
||||
}
|
||||
|
||||
fun refreshGroupTabTitles(refreshAll: Boolean = false) {
|
||||
val groupsToRefresh = if (refreshAll || mainViewModel.subscriptionId.isEmpty()) {
|
||||
groupPagerAdapter.groups
|
||||
} else {
|
||||
groupPagerAdapter.groups.filter { it.id == mainViewModel.subscriptionId }
|
||||
}
|
||||
|
||||
groupsToRefresh.forEach { group ->
|
||||
if (group.id.isEmpty()) {
|
||||
return@forEach
|
||||
}
|
||||
val tabIndex = groupPagerAdapter.groups.indexOfFirst { it.id == group.id }
|
||||
if (tabIndex >= 0) {
|
||||
val count = MmkvManager.decodeServerList(group.id).size
|
||||
binding.tabGroup.getTabAt(tabIndex)?.text = "${group.remarks} ($count)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFabAction() {
|
||||
@@ -304,12 +333,6 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
true
|
||||
}
|
||||
|
||||
R.id.ping_all -> {
|
||||
toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count()))
|
||||
mainViewModel.testAllTcping()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.real_ping_all -> {
|
||||
toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count()))
|
||||
mainViewModel.testAllRealPing()
|
||||
@@ -416,6 +439,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
count > 0 -> {
|
||||
toast(getString(R.string.title_import_config_count, count))
|
||||
mainViewModel.reloadServerList()
|
||||
refreshGroupTabTitles()
|
||||
}
|
||||
|
||||
countSub > 0 -> setupGroupTab()
|
||||
@@ -471,6 +495,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
}
|
||||
if (result.configCount > 0) {
|
||||
mainViewModel.reloadServerList()
|
||||
refreshGroupTabTitles()
|
||||
}
|
||||
hideLoading()
|
||||
}
|
||||
@@ -500,6 +525,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
val ret = mainViewModel.removeAllServer()
|
||||
launch(Dispatchers.Main) {
|
||||
mainViewModel.reloadServerList()
|
||||
refreshGroupTabTitles()
|
||||
toast(getString(R.string.title_del_config_count, ret))
|
||||
hideLoading()
|
||||
}
|
||||
@@ -519,6 +545,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
val ret = mainViewModel.removeDuplicateServer()
|
||||
launch(Dispatchers.Main) {
|
||||
mainViewModel.reloadServerList()
|
||||
refreshGroupTabTitles()
|
||||
toast(getString(R.string.title_del_duplicate_config_count, ret))
|
||||
hideLoading()
|
||||
}
|
||||
@@ -538,6 +565,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
val ret = mainViewModel.removeInvalidServer()
|
||||
launch(Dispatchers.Main) {
|
||||
mainViewModel.reloadServerList()
|
||||
refreshGroupTabTitles()
|
||||
toast(getString(R.string.title_del_config_count, ret))
|
||||
hideLoading()
|
||||
}
|
||||
|
||||
@@ -159,8 +159,12 @@ class MainRecyclerAdapter(
|
||||
|
||||
// Security: hide blank or tls
|
||||
profile.security?.let { sec ->
|
||||
if (sec.isNotBlank() && !sec.equals("tls", ignoreCase = true)) {
|
||||
parts.add(sec)
|
||||
if (sec.isNotBlank()) {
|
||||
if (profile.insecure == true && sec.equals("tls", ignoreCase = true)) {
|
||||
parts.add("$sec insecure") // TODO
|
||||
} else {
|
||||
parts.add(sec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.DEFAULT_PORT
|
||||
import com.v2ray.ang.AppConfig.REALITY
|
||||
@@ -27,10 +29,14 @@ import com.v2ray.ang.extension.nullIfBlank
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.AngConfigManager
|
||||
import com.v2ray.ang.handler.CertificateFingerprintManager
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsChangeManager
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ServerActivity : BaseActivity() {
|
||||
|
||||
@@ -140,7 +146,10 @@ class ServerActivity : BaseActivity() {
|
||||
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
|
||||
private val et_ech_config_list: EditText? by lazy { findViewById(R.id.et_ech_config_list) }
|
||||
private val container_ech_config_list: LinearLayout? by lazy { findViewById(R.id.lay_ech_config_list) }
|
||||
private val et_verify_peer_cert_by_name: EditText? by lazy { findViewById(R.id.et_verify_peer_cert_by_name) }
|
||||
private val container_verify_peer_cert_by_name: LinearLayout? by lazy { findViewById(R.id.lay_verify_peer_cert_by_name) }
|
||||
private val et_pinned_ca256: EditText? by lazy { findViewById(R.id.et_pinned_ca256) }
|
||||
private val btn_pinned_ca256_action: Button? by lazy { findViewById(R.id.btn_pinned_ca256_action) }
|
||||
private val container_pinned_ca256: LinearLayout? by lazy { findViewById(R.id.lay_pinned_ca256) }
|
||||
private val layout_browser_dialer: LinearLayout? by lazy { findViewById(R.id.layout_browser_dialer) }
|
||||
private val sp_browser_dialer_mode: Spinner? by lazy { findViewById(R.id.sp_browser_dialer_mode) }
|
||||
@@ -295,6 +304,7 @@ class ServerActivity : BaseActivity() {
|
||||
container_spider_x,
|
||||
container_mldsa65_verify,
|
||||
container_ech_config_list,
|
||||
container_verify_peer_cert_by_name,
|
||||
container_pinned_ca256
|
||||
).forEach { it?.visibility = View.GONE }
|
||||
}
|
||||
@@ -307,6 +317,7 @@ class ServerActivity : BaseActivity() {
|
||||
container_alpn,
|
||||
container_allow_insecure,
|
||||
container_ech_config_list,
|
||||
container_verify_peer_cert_by_name,
|
||||
container_pinned_ca256
|
||||
).forEach { it?.visibility = View.VISIBLE }
|
||||
listOf(
|
||||
@@ -327,6 +338,7 @@ class ServerActivity : BaseActivity() {
|
||||
container_alpn,
|
||||
container_allow_insecure,
|
||||
container_ech_config_list,
|
||||
container_verify_peer_cert_by_name,
|
||||
container_pinned_ca256
|
||||
).forEach { it?.visibility = View.GONE }
|
||||
listOf(
|
||||
@@ -343,6 +355,9 @@ class ServerActivity : BaseActivity() {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
btn_pinned_ca256_action?.setOnClickListener {
|
||||
fetchPinnedCA256ForCurrentConfig()
|
||||
}
|
||||
if (config != null) {
|
||||
bindingServer(config)
|
||||
} else {
|
||||
@@ -410,6 +425,7 @@ class ServerActivity : BaseActivity() {
|
||||
sp_allow_insecure?.setSelection(allowinsecure)
|
||||
}
|
||||
et_ech_config_list?.text = Utils.getEditable(config.echConfigList)
|
||||
et_verify_peer_cert_by_name?.text = Utils.getEditable(config.verifyPeerCertByName)
|
||||
et_pinned_ca256?.text = Utils.getEditable(config.pinnedCA256)
|
||||
} else if (config.security == REALITY) {
|
||||
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
|
||||
@@ -613,6 +629,7 @@ class ServerActivity : BaseActivity() {
|
||||
val spiderX = et_spider_x?.text?.toString()
|
||||
val mldsa65Verify = et_mldsa65_verify?.text?.toString()
|
||||
val echConfigList = et_ech_config_list?.text?.toString()
|
||||
val verifyPeerCertByName = et_verify_peer_cert_by_name?.text?.toString()
|
||||
val pinnedCA256 = et_pinned_ca256?.text?.toString()
|
||||
|
||||
val allowInsecure =
|
||||
@@ -632,9 +649,51 @@ class ServerActivity : BaseActivity() {
|
||||
config.spiderX = spiderX
|
||||
config.mldsa65Verify = mldsa65Verify
|
||||
config.echConfigList = echConfigList
|
||||
config.verifyPeerCertByName = verifyPeerCertByName
|
||||
config.pinnedCA256 = pinnedCA256
|
||||
}
|
||||
|
||||
private fun fetchPinnedCA256ForCurrentConfig() {
|
||||
val config = buildCurrentProfileForCertificateFetch() ?: return
|
||||
|
||||
lifecycleScope.launch {
|
||||
btn_pinned_ca256_action?.isEnabled = false
|
||||
try {
|
||||
val sha256 = withContext(Dispatchers.IO) {
|
||||
CertificateFingerprintManager.fetchForManualFill(config)
|
||||
}
|
||||
if (sha256.isNullOrBlank()) {
|
||||
toast(R.string.toast_fetch_cert_sha256_failed)
|
||||
} else {
|
||||
et_pinned_ca256?.text = Utils.getEditable(sha256)
|
||||
toastSuccess(R.string.toast_fetch_cert_sha256_success)
|
||||
}
|
||||
} finally {
|
||||
btn_pinned_ca256_action?.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildCurrentProfileForCertificateFetch(): ProfileItem? {
|
||||
if (TextUtils.isEmpty(et_address.text.toString())) {
|
||||
toast(R.string.server_lab_address)
|
||||
return null
|
||||
}
|
||||
|
||||
val configType = MmkvManager.decodeServerConfig(editGuid)?.configType ?: createConfigType
|
||||
if (configType != EConfigType.HYSTERIA2 && Utils.parseInt(et_port.text.toString()) <= 0) {
|
||||
toast(R.string.server_lab_port)
|
||||
return null
|
||||
}
|
||||
|
||||
val config = ProfileItem.create(configType)
|
||||
saveCommon(config)
|
||||
saveStreamSettings(config)
|
||||
saveTls(config)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
private fun transportTypes(network: String?): Array<out String> {
|
||||
return when (network) {
|
||||
NetworkType.TCP.type -> {
|
||||
|
||||
@@ -25,14 +25,10 @@ import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.AngConfigManager
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.handler.SpeedtestManager
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Collections
|
||||
@@ -46,7 +42,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val isRunning by lazy { MutableLiveData<Boolean>() }
|
||||
val updateListAction by lazy { MutableLiveData<Int>() }
|
||||
val updateTestResultAction by lazy { MutableLiveData<String>() }
|
||||
private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
|
||||
|
||||
/**
|
||||
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
||||
@@ -64,8 +59,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
*/
|
||||
override fun onCleared() {
|
||||
getApplication<AngApplication>().unregisterReceiver(mMsgReceiver)
|
||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||
SpeedtestManager.closeAllTcpSockets()
|
||||
LogUtil.i(AppConfig.TAG, "Main ViewModel is cleared")
|
||||
super.onCleared()
|
||||
}
|
||||
@@ -178,32 +171,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the TCP ping for all servers.
|
||||
*/
|
||||
fun testAllTcping() {
|
||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||
SpeedtestManager.closeAllTcpSockets()
|
||||
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
||||
|
||||
val serversCopy = serversCache.toList()
|
||||
for (item in serversCopy) {
|
||||
item.profile.let { outbound ->
|
||||
val serverAddress = outbound.server
|
||||
val serverPort = outbound.serverPort
|
||||
if (serverAddress != null && serverPort != null) {
|
||||
tcpingTestScope.launch {
|
||||
val testResult = SpeedtestManager.tcping(serverAddress, serverPort.toInt())
|
||||
launch(Dispatchers.Main) {
|
||||
MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
|
||||
updateListAction.value = getPosition(item.guid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the real ping for all servers.
|
||||
*/
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:layout_marginBottom="@dimen/padding_spacing_dp8"
|
||||
android:entries="@array/allowinsecures"
|
||||
android:nextFocusDown="@+id/et_ech_config_list"/>
|
||||
android:nextFocusDown="@+id/et_verify_peer_cert_by_name"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
@@ -138,6 +138,27 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/lay_verify_peer_cert_by_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/padding_spacing_dp16"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/server_lab_verify_peer_cert_by_name" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_verify_peer_cert_by_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:nextFocusDown="@+id/et_ech_config_list" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/lay_pinned_ca256"
|
||||
android:layout_width="match_parent"
|
||||
@@ -157,6 +178,14 @@
|
||||
android:inputType="text"
|
||||
android:nextFocusDown="@+id/et_public_key" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_pinned_ca256_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:layout_marginBottom="@dimen/padding_spacing_dp8"
|
||||
android:text="@string/pinned_ca256_action_fetch" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@@ -88,6 +88,14 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_pinned_ca256_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:layout_marginBottom="@dimen/padding_spacing_dp8"
|
||||
android:text="@string/pinned_ca256_action_fetch" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -90,10 +90,6 @@
|
||||
android:icon="@drawable/ic_share_24dp"
|
||||
android:title="@string/title_export_all"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/ping_all"
|
||||
android:title="@string/title_ping_all_server"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/real_ping_all"
|
||||
android:title="@string/title_real_ping_all_server"
|
||||
|
||||
@@ -118,7 +118,11 @@
|
||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Verify Peer Cert By Name</string>
|
||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||
<string name="pinned_ca256_action_fetch">Fetch and fill certificate fingerprint</string>
|
||||
<string name="toast_fetch_cert_sha256_success">Certificate fingerprint fetched successfully</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">Failed to fetch certificate fingerprint</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">فشل نسخ الملف، يرجى استخدام مدير الملفات</string>
|
||||
@@ -302,7 +306,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</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"> اختبر جميع الإعدادات (3)</string>
|
||||
<string name="title_user_asset_setting">Asset files</string>
|
||||
<string name="title_sort_by_test_results">الفرز حسب نتائج الاختبار (5)</string>
|
||||
|
||||
@@ -117,7 +117,11 @@
|
||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Verify Peer Cert By Name</string>
|
||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||
<string name="pinned_ca256_action_fetch">Fetch and fill certificate fingerprint</string>
|
||||
<string name="toast_fetch_cert_sha256_success">Certificate fingerprint fetched successfully</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">Failed to fetch certificate fingerprint</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">ফাইল কপি ব্যর্থ, অনুগ্রহ করে ফাইল ম্যানেজার ব্যবহার করুন</string>
|
||||
@@ -302,7 +306,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
|
||||
<string name="toast_invalid_update_interval">আপডেটের বিরতি সঠিক নয়। সর্বনিম্ন ১৫ মিনিট।</string>
|
||||
<string name="title_sub_update">সাবস্ক্রিপশন আপডেট</string>
|
||||
<string name="title_ping_all_server">সব কনফিগারেশন TCPing</string>
|
||||
<string name="title_real_ping_all_server">সব কনফিগারেশন প্রকৃত বিলম্ব</string>
|
||||
<string name="title_user_asset_setting">Asset files</string>
|
||||
<string name="title_sort_by_test_results">টেস্ট ফলাফল দ্বারা সাজানো</string>
|
||||
|
||||
@@ -117,7 +117,11 @@
|
||||
<string name="server_lab_xhttp_extra">XHTTP Extra خام JSON، قالوو: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask خام JSON، قالوو: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">نومگه کانفیگ Ech</string>
|
||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">تئیڌ گوواهی هومتا و ری نوم</string>
|
||||
<string name="server_lab_pinned_ca256">جا کلک گوواهی (SHA-256)</string>
|
||||
<string name="pinned_ca256_action_fetch">گرؽڌن وو پور کردن جا کلک گوواهی</string>
|
||||
<string name="toast_fetch_cert_sha256_success">جا کلک گوواهی وا مووفقیت گرؽڌه وابی</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">گرؽڌن جا کلک گوواهی مووفق نبی</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">لف گیری فایل ٱنجوم نوابی، ز ی برنومه دؽوۉداری فایل هیاری بگرین</string>
|
||||
@@ -302,7 +306,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">موتمعن بۊ ک نوم موستعار هڌس وو جۊرس نی</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>
|
||||
<string name="title_user_asset_setting">فایلا بونچک جوقرافیایی</string>
|
||||
<string name="title_sort_by_test_results">ترتیب و ری نتیجه یل آزمایش</string>
|
||||
@@ -357,7 +360,7 @@
|
||||
<string name="connection_not_connected">منپیز نؽڌ</string>
|
||||
<string name="connection_runing_task_left">تعداد وزیفه یل آزمایشی ک ره اۊفتانه: %s</string>
|
||||
|
||||
<string name="import_subscription_success">اشتراک وا موفقیت زفت زابی</string>
|
||||
<string name="import_subscription_success">اشتراک وا مووفقیت زفت وابی</string>
|
||||
<string name="import_subscription_failure">اشتراک زفت نوابی</string>
|
||||
<string name="title_fragment_settings">سامووا فرگمنت</string>
|
||||
<string name="title_pref_fragment_packets">کتنا فرگمنت</string>
|
||||
@@ -445,7 +448,7 @@
|
||||
</string-array>
|
||||
<string-array name="policy_group_type">
|
||||
<item>کم ترین پینگ</item>
|
||||
<item>کم ترین بار(لود)</item>
|
||||
<item>کم ترین بار (لود)</item>
|
||||
<item>تساڌۊفی</item>
|
||||
<item>گردشی</item>
|
||||
</string-array>
|
||||
|
||||
@@ -117,7 +117,11 @@
|
||||
<string name="server_lab_xhttp_extra">خام JSON XHTTP Extra، قالب: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">لیست کانفیگ Ech</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Verify Peer Cert By Name</string>
|
||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||
<string name="pinned_ca256_action_fetch">Fetch and fill certificate fingerprint</string>
|
||||
<string name="toast_fetch_cert_sha256_success">Certificate fingerprint fetched successfully</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">Failed to fetch certificate fingerprint</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">کپی فایل انجام نشد، لطفا از برنامه مدیریت فایل استفاده کنید</string>
|
||||
@@ -299,7 +303,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">لطفاً مطمئن شوید که نام مستعار وجود دارد و منحصر به فرد است</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>
|
||||
<string name="title_user_asset_setting">فایل های منبع جغرافیایی</string>
|
||||
<string name="title_sort_by_test_results">مرتب سازی بر اساس نتایج آزمایش</string>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<string name="menu_item_import_config_qrcode">Импорт из QR-кода</string>
|
||||
<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_policy_group">Добавить политику группы</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>
|
||||
@@ -117,9 +117,14 @@
|
||||
<string name="server_lab_xhttp_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">Необработанный JSON FinalMask, формат: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">ECHConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Проверять сертификат узла по имени</string>
|
||||
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
|
||||
<string name="server_lab_browser_dialer">Использовать переадресацию браузера</string>
|
||||
<string name="server_lab_browser_dialer_tip">Поддерживаются только исходящие соединения XHTTP (packet-up) и WS; настройки, связанные с TLS, могут быть проигнорированы или конфликтовать</string>
|
||||
<string name="toast_allow_insecure_deprecated">ВНИМАНИЕ: пропуск проверки сертификата («Разрешать небезопасные соединения») будет отключён в августе 2026 года. Пожалуйста, используйте отпечатки сертификатов как можно скорее. Функция будет недоступна после истечения срока действия.</string>
|
||||
<string name="pinned_ca256_action_fetch">Получить и заполнить отпечаток сертификата</string>
|
||||
<string name="toast_fetch_cert_sha256_success">Отпечаток сертификата успешно получен</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">Отпечаток сертификата не получен</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">Невозможно скопировать файл, используйте файловый менеджер</string>
|
||||
@@ -288,7 +293,7 @@
|
||||
<string name="title_logcat">Журнал</string>
|
||||
<string name="logcat_copy">Копировать</string>
|
||||
<string name="logcat_clear">Очистить</string>
|
||||
<string name="logcat_share">Поделиться логом</string>
|
||||
<string name="logcat_share">Поделиться журналом</string>
|
||||
<string name="title_service_restart">Перезапуск службы</string>
|
||||
<string name="title_del_all_config">Удалить профили</string>
|
||||
<string name="title_del_duplicate_config">Удалить дубликаты профилей</string>
|
||||
@@ -307,7 +312,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">Профиль должен быть уникальным</string>
|
||||
<string name="toast_invalid_update_interval">Неправильный интервал обновления. Минимум 15 минут.</string>
|
||||
<string name="title_sub_update">Обновить подписку</string>
|
||||
<string name="title_ping_all_server">Проверить профили</string>
|
||||
<string name="title_real_ping_all_server">Проверить задержку профилей</string>
|
||||
<string name="title_user_asset_setting">Файлы ресурсов</string>
|
||||
<string name="title_sort_by_test_results">Сортировать по результатам теста</string>
|
||||
@@ -377,7 +381,7 @@
|
||||
<string name="update_check_pre_release">Искать предварительный выпуск</string>
|
||||
<string name="update_checking_for_update">Проверка обновления…</string>
|
||||
|
||||
<string name="title_policy_group_type">Тип группы политик</string>
|
||||
<string name="title_policy_group_type">Тип политики группы</string>
|
||||
<string name="title_policy_group_subscription_id">Из группы подписки</string>
|
||||
<string name="title_policy_group_subscription_filter">Название фильтра</string>
|
||||
|
||||
@@ -424,6 +428,12 @@
|
||||
<item>Прокси</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="browser_dialer_mode">
|
||||
<item>Отключено</item>
|
||||
<item>OkHttp</item>
|
||||
<item>WebView</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="ui_mode_night">
|
||||
<item>Как в системе</item>
|
||||
<item>Светлая</item>
|
||||
|
||||
@@ -117,7 +117,11 @@
|
||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Verify Peer Cert By Name</string>
|
||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||
<string name="pinned_ca256_action_fetch">Fetch and fill certificate fingerprint</string>
|
||||
<string name="toast_fetch_cert_sha256_success">Certificate fingerprint fetched successfully</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">Failed to fetch certificate fingerprint</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">Không thể sao chép tệp tin, hãy dùng trình quản lý tệp!</string>
|
||||
@@ -302,7 +306,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
|
||||
<string name="toast_invalid_update_interval">Khoảng thời gian cập nhật không hợp lệ. Giá trị tối thiểu là 15 phút.</string>
|
||||
<string name="title_sub_update">Cập nhật các gói đăng ký</string>
|
||||
<string name="title_ping_all_server">Ping tất cả máy chủ</string>
|
||||
<string name="title_real_ping_all_server">Kiểm tra HTTP tất cả máy chủ</string>
|
||||
<string name="title_user_asset_setting">Asset files</string>
|
||||
<string name="title_sort_by_test_results">Sắp xếp lại theo lần kiểm tra cuối cùng</string>
|
||||
|
||||
@@ -117,10 +117,14 @@
|
||||
<string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">FinalMask 原始 JSON 格式: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Verify Peer Cert By Name</string>
|
||||
<string name="server_lab_pinned_ca256">证书指纹 (SHA-256)</string>
|
||||
<string name="server_lab_browser_dialer">启用浏览器转发</string>
|
||||
<string name="server_lab_browser_dialer_tip">仅支持 xhttp (packet-up) 和 ws。与优选域名冲突,utls, alpn, ech 等 TLS 设置将被忽略</string>
|
||||
<string name="toast_allow_insecure_deprecated">警告:将在 2026.8.1 禁用跳过证书验证 "allowInsecure" ,请尽快改用证书指纹。到期后无法使用。 </string>
|
||||
<string name="toast_allow_insecure_deprecated">警告:将在 2026.8.1 移除跳过证书验证 "allowInsecure" ,请尽快改用证书指纹或修改服务端。否则到期后会无法使用。 </string>
|
||||
<string name="pinned_ca256_action_fetch">获取并填充指纹证书</string>
|
||||
<string name="toast_fetch_cert_sha256_success">获取指纹证书完成</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">获取证书指纹失败</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">失败, 请使用文件管理器</string>
|
||||
@@ -303,7 +307,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">请确保配置别名存在并唯一</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>
|
||||
<string name="title_user_asset_setting">资源文件</string>
|
||||
<string name="title_sort_by_test_results">按测试结果排序</string>
|
||||
|
||||
@@ -117,8 +117,12 @@
|
||||
<string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">FinalMask 原始 JSON 格式: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Verify Peer Cert By Name</string>
|
||||
<string name="server_lab_pinned_ca256">證書指紋 (SHA-256)</string>
|
||||
<string name="toast_allow_insecure_deprecated">警告:將在 2026.8.1 停用跳過憑證驗證 "allowInsecure" ,請盡快改用憑證指紋。到期後無法使用。 </string>
|
||||
<string name="pinned_ca256_action_fetch">獲取並填充指紋證書</string>
|
||||
<string name="toast_fetch_cert_sha256_success">獲取指紋證書完成</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">獲取證書指紋失敗</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">失敗,請使用檔案總管</string>
|
||||
@@ -302,7 +306,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">请确保設定檔别名存在并唯一</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>
|
||||
<string name="title_user_asset_setting">資源檔案</string>
|
||||
<string name="title_sort_by_test_results">依偵測結果排序</string>
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
|
||||
<string-array name="fragment_packets" translatable="false">
|
||||
<item>tlshello</item>
|
||||
<item>1-1</item>
|
||||
<item>1-2</item>
|
||||
<item>1-3</item>
|
||||
<item>1-5</item>
|
||||
|
||||
@@ -118,10 +118,14 @@
|
||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Verify Peer Cert By Name</string>
|
||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||
<string name="server_lab_browser_dialer">Enable Browser Dialer</string>
|
||||
<string name="server_lab_browser_dialer_tip">Only supports xhttp (packet-up) and ws outbound; TLS-related settings may be ignored or conflict</string>
|
||||
<string name="toast_allow_insecure_deprecated">WARNING: Skipping certificate verification "allowInsecure" will be disabled in August 2026. Please use certificate fingerprints as soon as possible. This feature will not be available after its expiration.</string>
|
||||
<string name="pinned_ca256_action_fetch">Fetch and fill certificate fingerprint</string>
|
||||
<string name="toast_fetch_cert_sha256_success">Certificate fingerprint fetched successfully</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">Failed to fetch certificate fingerprint</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">File copy failed, please use File Manager</string>
|
||||
@@ -310,7 +314,6 @@
|
||||
<string name="sub_setting_pre_profile_tip">The config remarks exist and are unique</string>
|
||||
<string name="toast_invalid_update_interval">Invalid update interval. Minimum is 15 minutes.</string>
|
||||
<string name="title_sub_update">Update subscription</string>
|
||||
<string name="title_ping_all_server">Tcping config</string>
|
||||
<string name="title_real_ping_all_server">Real delay config</string>
|
||||
<string name="title_user_asset_setting">Asset files</string>
|
||||
<string name="title_sort_by_test_results">Sorting by test results</string>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
agp = "9.2.1"
|
||||
desugarJdkLibs = "2.1.5"
|
||||
gradleLicensePlugin = "0.9.8"
|
||||
kotlin = "2.3.21"
|
||||
kotlin = "2.4.0"
|
||||
coreKtx = "1.17.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.3.0"
|
||||
|
||||
Reference in new issue
Block a user