Compare commits

...

18 Commits

Author SHA1 Message Date
2dust
6632ba3b6e Update libs.versions.toml 2025-08-25 18:39:00 +08:00
2dust
bd582f1d90 Update libs.versions.toml 2025-08-25 18:32:15 +08:00
Evgenii Pravda
f5f1e12565 Reasonable app sorting order (#4869) 2025-08-25 17:49:24 +08:00
2dust
207d6f4f8c Revert "Update build.yml"
This reverts commit fc0e60a097.
2025-08-17 11:15:48 +08:00
2dust
52e0a19826 up 1.10.19 2025-08-17 10:22:04 +08:00
2dust
fc0e60a097 Update build.yml 2025-08-17 09:41:41 +08:00
Hossin Asaadi
65d6b4aaa8 fix redirect infinite loop (#4857) 2025-08-17 09:27:22 +08:00
Hossin Asaadi
7b11755e7f IPv6 Unreachability Fallback for TLS Configs (#4846)
* fix unreachable ipv6 fallback

* add UseIP domainStrategy

* fix DNS query loop
2025-08-16 14:34:53 +08:00
fuilloi
2d0de4860c fix (#4855) 2025-08-16 14:06:35 +08:00
solokot
7406ef16ff Update Russian translation (#4845) 2025-08-14 17:33:24 +08:00
DHR60
a084b21d50 Improves intelligent selection toast and DNS routing (#4838)
* Improves intelligent selection toast and DNS routing

* rename
2025-08-13 16:59:22 +08:00
2dust
bf01fe2bdb up 1.10.18 2025-08-13 08:48:33 +08:00
Skh-web6982
519cc2a4b5 Bump actions/checkout from 4 to 5 (#4837)
Bump actions/checkout from 4 to 5
2025-08-13 08:38:18 +08:00
Tamim Hossain
c21653d40f Feat/add mtu in settings (#4836)
* Added MTU In Settings

Added MTU In Settings.
Closes  #4824

* Update SettingsViewModel.kt
2025-08-13 08:38:07 +08:00
solokot
1919c5e05f Update Russian translation (#4833) 2025-08-13 08:37:52 +08:00
2dust
8a17d93882 up 1.10.17 2025-08-12 18:04:41 +08:00
2dust
21ed008c7b up strings 2025-08-12 18:02:22 +08:00
Tamim Hossain
d83cfa28c2 Added MTU In Settings (#4828)
Added MTU In Settings.
Closes  #4824
2025-08-12 17:33:04 +08:00
25 changed files with 138 additions and 45 deletions

View File

@@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4.2.2
uses: actions/checkout@v5
with:
submodules: 'recursive'
fetch-depth: '0'

View File

@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang"
minSdk = 21
targetSdk = 35
versionCode = 666
versionName = "1.10.16"
versionCode = 669
versionName = "1.10.19"
multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')

View File

@@ -27,6 +27,7 @@ object AppConfig {
const val PREF_VPN_DNS = "pref_vpn_dns"
const val PREF_VPN_BYPASS_LAN = "pref_vpn_bypass_lan"
const val PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX = "pref_vpn_interface_address_config_index"
const val PREF_VPN_MTU = "pref_vpn_mtu"
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
const val PREF_ROUTING_RULESET = "pref_routing_ruleset"
const val PREF_MUX_ENABLED = "pref_mux_enabled"
@@ -90,6 +91,8 @@ object AppConfig {
const val TAG_DIRECT = "direct"
const val TAG_BLOCKED = "block"
const val TAG_FRAGMENT = "fragment"
const val TAG_DNS = "dns-module"
const val TAG_DOMESTIC_DNS = "domestic-dns"
/** Network-related constants. */
const val UPLINK = "uplink"

View File

@@ -245,7 +245,14 @@ data class V2rayConfig(
var tproxy: String? = null,
var mark: Int? = null,
var dialerProxy: String? = null,
var domainStrategy: String? = null
var domainStrategy: String? = null,
var happyEyeballs: happyEyeballsBean? = null,
)
data class happyEyeballsBean(
var prioritizeIPv6: Boolean? = null,
var maxConcurrentTry: Int? = 4,
var tryDelayMs: Int? = 250, // ms
var interleave: Int? = null,
)
data class TlsSettingsBean(
@@ -490,6 +497,7 @@ data class V2rayConfig(
var expectIPs: List<String>? = null,
val clientIp: String? = null,
val skipFallback: Boolean? = null,
val tag: String? = null,
)
}

View File

@@ -370,4 +370,11 @@ object SettingsManager {
val selectedIndex = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX, "0")?.toInt()
return VpnInterfaceAddressConfig.getConfigByIndex(selectedIndex ?: 0)
}
/**
* Get the VPN MTU from settings, defaulting to AppConfig.VPN_MTU.
*/
fun getVpnMtu(): Int {
return Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_MTU), AppConfig.VPN_MTU)
}
}

View File

@@ -509,9 +509,10 @@ object V2rayConfigManager {
//hev-socks5-tunnel dns routing
v2rayConfig.routing.rules.add(
0, RulesBean(
type = "field",
inboundTag = arrayListOf("socks"),
outboundTag = "dns-out",
port = "53",
outboundTag = "dns-out"
type = "field"
)
)
}
@@ -574,18 +575,8 @@ object V2rayConfigManager {
address = domesticDns.first(),
domains = directDomain,
expectIPs = if (isCnRoutingMode) geoipCn else null,
skipFallback = true
)
)
}
if (Utils.isPureIpAddress(domesticDns.first())) {
v2rayConfig.routing.rules.add(
0, RulesBean(
outboundTag = AppConfig.TAG_DIRECT,
port = "53",
ip = arrayListOf(domesticDns.first()),
domain = null
skipFallback = true,
tag = AppConfig.TAG_DOMESTIC_DNS
)
)
}
@@ -626,20 +617,26 @@ object V2rayConfigManager {
// DNS dns
v2rayConfig.dns = V2rayConfig.DnsBean(
servers = servers,
hosts = hosts
hosts = hosts,
tag = AppConfig.TAG_DNS
)
// DNS routing
if (Utils.isPureIpAddress(remoteDns.first())) {
v2rayConfig.routing.rules.add(
0, RulesBean(
outboundTag = AppConfig.TAG_PROXY,
port = "53",
ip = arrayListOf(remoteDns.first()),
domain = null
)
v2rayConfig.routing.rules.add(
0, RulesBean(
outboundTag = AppConfig.TAG_PROXY,
inboundTag = arrayListOf(AppConfig.TAG_DNS),
domain = null
)
}
)
v2rayConfig.routing.rules.add(
0, RulesBean(
outboundTag = AppConfig.TAG_DIRECT,
inboundTag = arrayListOf(AppConfig.TAG_DOMESTIC_DNS),
domain = null
)
)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to configure DNS", e)
return false
@@ -1014,14 +1011,22 @@ object V2rayConfigManager {
if (domain.isNullOrEmpty()) continue
if (newHosts.containsKey(domain)) {
item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6"
item.ensureSockopt().domainStrategy = "UseIP"
item.ensureSockopt().happyEyeballs = StreamSettingsBean.happyEyeballsBean(
prioritizeIPv6 = preferIpv6,
interleave = 2
)
continue
}
val resolvedIps = HttpUtil.resolveHostToIP(domain, preferIpv6)
if (resolvedIps.isNullOrEmpty()) continue
item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6"
item.ensureSockopt().domainStrategy = "UseIP"
item.ensureSockopt().happyEyeballs = StreamSettingsBean.happyEyeballsBean(
prioritizeIPv6 = preferIpv6,
interleave = 2
)
newHosts[domain] = if (resolvedIps.size == 1) {
resolvedIps[0]
} else {

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.os.ParcelFileDescriptor
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.VPN_MTU
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import java.io.File
@@ -60,7 +59,7 @@ class TProxyService(
val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig()
return buildString {
appendLine("tunnel:")
appendLine(" mtu: $VPN_MTU")
appendLine(" mtu: ${SettingsManager.getVpnMtu()}")
appendLine(" ipv4: ${vpnConfig.ipv4Client}")
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) {

View File

@@ -6,7 +6,6 @@ import android.net.LocalSocketAddress
import android.os.ParcelFileDescriptor
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.VPN_MTU
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.Utils
@@ -42,7 +41,7 @@ class Tun2SocksService(
"--netif-ipaddr", vpnConfig.ipv4Router,
"--netif-netmask", "255.255.255.252",
"--socks-server-addr", "${AppConfig.LOOPBACK}:${socksPort}",
"--tunmtu", VPN_MTU.toString(),
"--tunmtu", SettingsManager.getVpnMtu().toString(),
"--sock-path", "sock_path",
"--enable-udprelay",
"--loglevel", "notice"

View File

@@ -17,7 +17,6 @@ import android.util.Log
import androidx.annotation.RequiresApi
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.AppConfig.VPN_MTU
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.NotificationManager
@@ -185,7 +184,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
val bypassLan = SettingsManager.routingRulesetsBypassLan()
// Configure IPv4 settings
builder.setMtu(VPN_MTU)
builder.setMtu(SettingsManager.getVpnMtu())
builder.addAddress(vpnConfig.ipv4Client, 30)
// Configure routing rules

View File

@@ -386,6 +386,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
R.id.intelligent_selection_all -> {
if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") != "0") {
toast(getString(R.string.pre_resolving_domain))
}
mainViewModel.createIntelligentSelectionAll()
true
}

View File

@@ -56,8 +56,14 @@ class PerAppProxyActivity : BaseActivity() {
appsList.sortedWith { p1, p2 ->
when {
p1.isSelected > p2.isSelected -> -1
p1.isSelected == p2.isSelected -> 0
else -> 1
p1.isSelected < p2.isSelected -> 1
p1.isSystemApp > p2.isSystemApp -> 1
p1.isSystemApp < p2.isSystemApp -> -1
p1.appName.lowercase() > p2.appName.lowercase() -> 1
p1.appName.lowercase() < p2.appName.lowercase() -> -1
p1.packageName > p2.packageName -> 1
p1.packageName < p2.packageName -> -1
else -> 0
}
}
} else {

View File

@@ -45,6 +45,7 @@ class SettingsActivity : BaseActivity() {
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
private val vpnBypassLan by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_BYPASS_LAN) }
private val vpnInterfaceAddress by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX) }
private val vpnMtu by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_MTU) }
private val mux by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_MUX_ENABLED) }
private val muxConcurrency by lazy { findPreference<EditTextPreference>(AppConfig.PREF_MUX_CONCURRENCY) }
@@ -93,6 +94,12 @@ class SettingsActivity : BaseActivity() {
true
}
vpnMtu?.setOnPreferenceChangeListener { _, any ->
val nval = any as String
vpnMtu?.summary = if (TextUtils.isEmpty(nval)) AppConfig.VPN_MTU.toString() else nval
true
}
mux?.setOnPreferenceChangeListener { _, newValue ->
updateMux(newValue as Boolean)
true
@@ -196,6 +203,7 @@ class SettingsActivity : BaseActivity() {
appendHttpProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_APPEND_HTTP_PROXY, false)
localDnsPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
vpnDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
vpnMtu?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_MTU, AppConfig.VPN_MTU.toString())
updateMux(MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false))
mux?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
@@ -229,6 +237,7 @@ class SettingsActivity : BaseActivity() {
listOf(
localDnsPort,
vpnDns,
vpnMtu,
muxConcurrency,
muxXudpConcurrency,
fragmentLength,
@@ -298,6 +307,7 @@ class SettingsActivity : BaseActivity() {
vpnDns?.isEnabled = vpn
vpnBypassLan?.isEnabled = vpn
vpnInterfaceAddress?.isEnabled = vpn
vpnMtu?.isEnabled = vpn
if (vpn) {
updateLocalDns(
MmkvManager.decodeSettingsBool(

View File

@@ -12,7 +12,9 @@ import java.net.IDN
import java.net.Inet6Address
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.MalformedURLException
import java.net.Proxy
import java.net.URI
import java.net.URL
object HttpUtil {
@@ -140,7 +142,7 @@ object HttpUtil {
val responseCode = conn.responseCode
when (responseCode) {
in 300..399 -> {
val location = conn.getHeaderField("Location")
val location = resolveLocation(conn)
conn.disconnect()
if (location.isNullOrEmpty()) {
throw IOException("Redirect location not found")
@@ -219,5 +221,29 @@ object HttpUtil {
}
return conn
}
// Returns absolute URL string location header sets
fun resolveLocation(conn: HttpURLConnection): String? {
val raw = conn.getHeaderField("Location")?.trim()?.takeIf { it.isNotEmpty() } ?: return null
// Try check url is relative or absolute
return try {
val locUri = URI(raw)
val baseUri = conn.url.toURI()
val resolved = if (locUri.isAbsolute) locUri else baseUri.resolve(locUri)
resolved.toURL().toString()
} catch (_: Exception) {
// Fallback: url resolver, also should handles //host/...
try {
URL(raw).toString() // absolute with protocol
} catch (_: MalformedURLException) {
try {
URL(conn.url, raw).toString()
} catch (_: MalformedURLException) {
null
}
}
}
}
}

View File

@@ -42,6 +42,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
AppConfig.PREF_VPN_DNS,
AppConfig.PREF_VPN_BYPASS_LAN,
AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX,
AppConfig.PREF_VPN_MTU,
AppConfig.PREF_REMOTE_DNS,
AppConfig.PREF_DOMESTIC_DNS,
AppConfig.PREF_DNS_HOSTS,

View File

@@ -81,6 +81,7 @@
<string name="server_lab_preshared_key">PreSharedKey(optional)</string>
<string name="server_lab_short_id" translatable="false">المعرّف القصير</string>
<string name="server_lab_spider_x" translatable="false">SpiderX</string>
<string name="server_lab_mldsa65_verify">Mldsa65Verify</string>
<string name="server_lab_secret_key" translatable="false">المفتاح السري</string>
<string name="server_lab_reserved">محجوز (اختياري)</string>
<string name="server_lab_local_address">العنوان المحلي (اختياري IPv4/IPv6، مفصولة بفواصل)</string>
@@ -183,6 +184,7 @@
<string name="title_pref_vpn_bypass_lan">Does VPN bypass LAN</string>
<string name="title_pref_vpn_interface_address">VPN Interface Address</string>
<string name="title_pref_vpn_mtu">VPN MTU (default 1500)</string>
<string name="title_pref_domestic_dns">DNS المحلي (اختياري)</string>
<string name="summary_pref_domestic_dns">DNS</string>
@@ -375,5 +377,6 @@
<item>Least Ping</item>
<item>Least Load</item>
</string-array>
<string name="pre_resolving_domain">Pre-resolving domain…</string>
</resources>

View File

@@ -184,6 +184,7 @@
<string name="title_pref_vpn_bypass_lan">Does VPN bypass LAN</string>
<string name="title_pref_vpn_interface_address">VPN Interface Address</string>
<string name="title_pref_vpn_mtu">VPN MTU (default 1500)</string>
<string name="title_pref_domestic_dns">ঘরোয়া DNS (ঐচ্ছিক)</string>
<string name="summary_pref_domestic_dns">DNS</string>
@@ -381,5 +382,6 @@
<item>Least Ping</item>
<item>Least Load</item>
</string-array>
<string name="pre_resolving_domain">Pre-resolving domain…</string>
</resources>

View File

@@ -184,6 +184,7 @@
<string name="title_pref_vpn_bypass_lan">VPN ز شبکه مهلی اگوڌرته؟</string>
<string name="title_pref_vpn_interface_address">نشۊوی رابت VPN</string>
<string name="title_pref_vpn_mtu">VPN MTU (default 1500)</string>
<string name="title_pref_domestic_dns">DNS منی (اختیاری)</string>
<string name="summary_pref_domestic_dns">DNS</string>
@@ -391,5 +392,6 @@
<item>کم ترین پینگ</item>
<item>کم ترین بار(لود)</item>
</string-array>
<string name="pre_resolving_domain">Pre-resolving domain…</string>
</resources>

View File

@@ -182,6 +182,7 @@
<string name="title_pref_vpn_bypass_lan">آیا VPN از شبکه محلی عبور می کند؟</string>
<string name="title_pref_vpn_interface_address">آدرس واسط VPN</string>
<string name="title_pref_vpn_mtu">VPN MTU (default 1500)</string>
<string name="title_pref_domestic_dns">DNS داخلی (اختیاری)</string>
<string name="summary_pref_domestic_dns">DNS</string>
@@ -390,5 +391,6 @@
<item>کمترین پینگ</item>
<item>کمترین بار(لود)</item>
</string-array>
<string name="pre_resolving_domain">Pre-resolving domain…</string>
</resources>

View File

@@ -183,6 +183,7 @@
<string name="title_pref_vpn_bypass_lan">VPN обходит LAN</string>
<string name="title_pref_vpn_interface_address">Адрес интерфейса VPN</string>
<string name="title_pref_vpn_mtu">VPN MTU (по умолчанию 1500)</string>
<string name="title_pref_domestic_dns">Внутренняя DNS (необязательно)</string>
<string name="summary_pref_domestic_dns">DNS</string>
@@ -197,7 +198,7 @@
<string name="summary_pref_proxy_sharing_enabled">Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать локальный прокси. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения.</string>
<string name="toast_warning_pref_proxysharing_short">Доступ из LAN разрешён, убедитесь, что вы находитесь в доверенной сети</string>
<string name="title_pref_allow_insecure">Разрешать небезопасные</string>
<string name="title_pref_allow_insecure">Разрешать небезопасные соединения</string>
<string name="summary_pref_allow_insecure">Для TLS по умолчанию разрешены небезопасные соединения</string>
<string name="title_pref_socks_port">Порт локального прокси</string>
@@ -390,5 +391,6 @@
<item>Наименьшая задержка</item>
<item>Наименьшая нагрузка</item>
</string-array>
<string name="pre_resolving_domain">Предварительное определение домена…</string>
</resources>

View File

@@ -184,6 +184,7 @@
<string name="title_pref_vpn_bypass_lan">Does VPN bypass LAN</string>
<string name="title_pref_vpn_interface_address">VPN Interface Address</string>
<string name="title_pref_vpn_mtu">VPN MTU (default 1500)</string>
<string name="title_pref_domestic_dns">DNS nội địa (Không bắt buộc)</string>
<string name="summary_pref_domestic_dns">DNS</string>
@@ -378,5 +379,6 @@
<item>Least Ping</item>
<item>Least Load</item>
</string-array>
<string name="pre_resolving_domain">Pre-resolving domain…</string>
</resources>

View File

@@ -181,6 +181,7 @@
<string name="title_pref_vpn_bypass_lan">VPN 是否绕过局域网</string>
<string name="title_pref_vpn_interface_address">VPN 接口地址</string>
<string name="title_pref_vpn_mtu">VPN MTU (默认 1500)</string>
<string name="title_pref_domestic_dns">境内 DNS (可选)</string>
<string name="summary_pref_domestic_dns">DNS</string>
@@ -382,5 +383,6 @@
<item>最低延迟</item>
<item>最稳定</item>
</string-array>
<string name="pre_resolving_domain">预解析域名中…</string>
</resources>

View File

@@ -183,6 +183,7 @@
<string name="title_pref_vpn_bypass_lan">VPN 是否繞過區域網</string>
<string name="title_pref_vpn_interface_address">VPN 介面位址</string>
<string name="title_pref_vpn_mtu">VPN MTU (預設 1500)</string>
<string name="summary_pref_domestic_dns">DNS</string>
<string name="title_pref_domestic_dns">境内 DNS (可选)</string>
@@ -382,5 +383,6 @@
<item>Least Ping</item>
<item>Least Load</item>
</string-array>
<string name="pre_resolving_domain">Pre-resolving domain…</string>
</resources>

View File

@@ -186,6 +186,9 @@
<string name="title_pref_vpn_interface_address">VPN Interface Address</string>
<string name="title_pref_vpn_mtu">VPN MTU (default 1500)</string>
<string name="title_pref_domestic_dns">Domestic DNS (Optional)</string>
<string name="summary_pref_domestic_dns">DNS</string>
@@ -392,5 +395,6 @@
<item>Least Ping</item>
<item>Least Load</item>
</string-array>
<string name="pre_resolving_domain">Pre-resolving domain…</string>
</resources>

View File

@@ -72,6 +72,12 @@
android:summary="%s"
android:title="@string/title_pref_vpn_interface_address" />
<EditTextPreference
android:inputType="number"
android:key="pref_vpn_mtu"
android:summary="1500"
android:title="@string/title_pref_vpn_mtu" />
<CheckBoxPreference
android:key="pref_use_hev_tunnel"
android:summary="@string/summary_pref_use_hev_tunnel"

View File

@@ -1,12 +1,12 @@
[versions]
agp = "8.12.0"
agp = "8.12.1"
desugarJdkLibs = "2.1.5"
gradleLicensePlugin = "0.9.8"
kotlin = "2.2.0"
kotlin = "2.2.10"
coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
appcompat = "1.7.1"
material = "1.12.0"
activity = "1.10.1"
@@ -20,7 +20,7 @@ swiperefreshlayout = "1.1.0"
toasty = "1.5.2"
editorkit = "2.9.0"
core = "3.5.3"
workRuntimeKtx = "2.10.2"
workRuntimeKtx = "2.10.3"
lifecycleViewmodelKtx = "2.9.2"
multidex = "2.0.1"
mockitoMockitoInline = "5.2.0"