Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51eabe5440 | ||
|
|
6f0b3ce990 | ||
|
|
69e27ed3bb | ||
|
|
fff6ab30e6 | ||
|
|
fdb67a86f4 | ||
|
|
ea088376ac | ||
|
|
52332d960e | ||
|
|
3ead542e2b | ||
|
|
9d1f98ff34 | ||
|
|
f305e26a39 | ||
|
|
aa47fba20d | ||
|
|
69c5bbfd3d | ||
|
|
90ed02804c | ||
|
|
822c1de79c | ||
|
|
d910b93525 | ||
|
|
7e6b1c247b | ||
|
|
f3f2b7fab5 | ||
|
|
e6f260da76 | ||
|
|
55bc2bf934 | ||
|
|
f22454da5d | ||
|
|
4a87549fa7 | ||
|
|
d447adc97f |
Submodule AndroidLibXrayLite updated: 5cdcbc611f...4de15a6c8d
@@ -3,16 +3,12 @@
|
|||||||
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
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://developer.android.com/about/versions/lollipop)
|
||||||
[](https://kotlinlang.org)
|
[](https://kotlinlang.org)
|
||||||
[](https://github.com/2dust/v2rayNG/commits/master)
|
[](https://github.com/2dust/v2rayNG/commits/master)
|
||||||
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
||||||
[](https://github.com/2dust/v2rayNG/releases)
|
[](https://github.com/2dust/v2rayNG/releases)
|
||||||
[](https://t.me/v2rayn)
|
[](https://t.me/v2rayn)
|
||||||
|
|
||||||
<a href="https://play.google.com/store/apps/details?id=com.v2ray.ang">
|
|
||||||
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="165" height="64" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
### Telegram Channel
|
### Telegram Channel
|
||||||
[github_2dust](https://t.me/github_2dust)
|
[github_2dust](https://t.me/github_2dust)
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 652
|
versionCode = 656
|
||||||
versionName = "1.10.2"
|
versionName = "1.10.6"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
|
|||||||
@@ -144,6 +144,9 @@
|
|||||||
<data android:host="install-sub" />
|
<data android:host="install-sub" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.CheckUpdateActivity"
|
||||||
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.AboutActivity"
|
android:name=".ui.AboutActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ object AppConfig {
|
|||||||
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
|
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
|
||||||
const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
|
const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
|
||||||
const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
|
const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
|
||||||
const val IP_API_Url = "https://api.ip.sb/geoip"
|
const val IP_API_URL = "https://speed.cloudflare.com/meta"
|
||||||
|
|
||||||
/** DNS server addresses. */
|
/** DNS server addresses. */
|
||||||
const val DNS_PROXY = "1.1.1.1"
|
const val DNS_PROXY = "1.1.1.1"
|
||||||
@@ -189,7 +189,7 @@ object AppConfig {
|
|||||||
val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
|
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
|
//minimum list https://serverfault.com/a/304791
|
||||||
val BYPASS_PRIVATE_IP_LIST = arrayListOf(
|
val ROUTED_IP_LIST = arrayListOf(
|
||||||
"0.0.0.0/5",
|
"0.0.0.0/5",
|
||||||
"8.0.0.0/7",
|
"8.0.0.0/7",
|
||||||
"11.0.0.0/8",
|
"11.0.0.0/8",
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ package com.v2ray.ang.dto
|
|||||||
|
|
||||||
data class IPAPIInfo(
|
data class IPAPIInfo(
|
||||||
var ip: String? = null,
|
var ip: String? = null,
|
||||||
var city: String? = null,
|
var clientIp: String? = null,
|
||||||
var region: String? = null,
|
var ip_addr: String? = null,
|
||||||
var region_code: String? = null,
|
var query: String? = null,
|
||||||
var country: String? = null,
|
var country: String? = null,
|
||||||
var country_name: String? = null,
|
var country_name: String? = null,
|
||||||
var country_code: String? = null
|
var country_code: String? = null,
|
||||||
|
var countryCode: String? = null
|
||||||
)
|
)
|
||||||
@@ -4,6 +4,7 @@ import com.v2ray.ang.AppConfig
|
|||||||
import com.v2ray.ang.dto.NetworkType
|
import com.v2ray.ang.dto.NetworkType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ open class FmtBase {
|
|||||||
val url = String.format(
|
val url = String.format(
|
||||||
"%s@%s:%s",
|
"%s@%s:%s",
|
||||||
Utils.urlEncode(userInfo ?: ""),
|
Utils.urlEncode(userInfo ?: ""),
|
||||||
Utils.getIpv6Address(config.server),
|
Utils.getIpv6Address(HttpUtil.toIdnDomain(config.server.orEmpty())),
|
||||||
config.serverPort
|
config.serverPort
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -149,6 +150,8 @@ open class FmtBase {
|
|||||||
return dicQuery
|
return dicQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getServerAddress(profileItem: ProfileItem): String {
|
||||||
|
return HttpUtil.toIdnDomain(profileItem.server.orEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ object HttpFmt : FmtBase() {
|
|||||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HTTP)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HTTP)
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
server.address = profileItem.server.orEmpty()
|
server.address = getServerAddress(profileItem)
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
if (profileItem.username.isNotNullEmpty()) {
|
if (profileItem.username.isNotNullEmpty()) {
|
||||||
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ object ShadowsocksFmt : FmtBase() {
|
|||||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SHADOWSOCKS)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SHADOWSOCKS)
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
server.address = profileItem.server.orEmpty()
|
server.address = getServerAddress(profileItem)
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
server.password = profileItem.password
|
server.password = profileItem.password
|
||||||
server.method = profileItem.method
|
server.method = profileItem.method
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ object SocksFmt : FmtBase() {
|
|||||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SOCKS)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SOCKS)
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
server.address = profileItem.server.orEmpty()
|
server.address = getServerAddress(profileItem)
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
if (profileItem.username.isNotNullEmpty()) {
|
if (profileItem.username.isNotNullEmpty()) {
|
||||||
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ object TrojanFmt : FmtBase() {
|
|||||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.TROJAN)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.TROJAN)
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
server.address = profileItem.server.orEmpty()
|
server.address = getServerAddress(profileItem)
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
server.password = profileItem.password
|
server.password = profileItem.password
|
||||||
server.flow = profileItem.flow
|
server.flow = profileItem.flow
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ object VlessFmt : FmtBase() {
|
|||||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VLESS)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VLESS)
|
||||||
|
|
||||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||||
vnext.address = profileItem.server.orEmpty()
|
vnext.address = getServerAddress(profileItem)
|
||||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
vnext.users[0].id = profileItem.password.orEmpty()
|
vnext.users[0].id = profileItem.password.orEmpty()
|
||||||
vnext.users[0].encryption = profileItem.method
|
vnext.users[0].encryption = profileItem.method
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ object VmessFmt : FmtBase() {
|
|||||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VMESS)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VMESS)
|
||||||
|
|
||||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||||
vnext.address = profileItem.server.orEmpty()
|
vnext.address = getServerAddress(profileItem)
|
||||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
vnext.users[0].id = profileItem.password.orEmpty()
|
vnext.users[0].id = profileItem.password.orEmpty()
|
||||||
vnext.users[0].security = profileItem.method
|
vnext.users[0].security = profileItem.method
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ object AngConfigManager {
|
|||||||
if (!it.second.enabled) {
|
if (!it.second.enabled) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
val url = HttpUtil.idnToASCII(it.second.url)
|
val url = HttpUtil.toIdnUrl(it.second.url)
|
||||||
if (!Utils.isValidUrl(url)) {
|
if (!Utils.isValidUrl(url)) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ object SettingsManager {
|
|||||||
* @return True if bypassing LAN, false otherwise.
|
* @return True if bypassing LAN, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun routingRulesetsBypassLan(): Boolean {
|
fun routingRulesetsBypassLan(): Boolean {
|
||||||
val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "0"
|
val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "1"
|
||||||
if (vpnBypassLan == "1") {
|
if (vpnBypassLan == "1") {
|
||||||
return true
|
return true
|
||||||
} else if (vpnBypassLan == "2") {
|
} else if (vpnBypassLan == "2") {
|
||||||
|
|||||||
@@ -168,10 +168,13 @@ object SpeedtestManager {
|
|||||||
|
|
||||||
fun getRemoteIPInfo(): String? {
|
fun getRemoteIPInfo(): String? {
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
var content = HttpUtil.getUrlContent(AppConfig.IP_API_Url, 5000, httpPort) ?: return null
|
var content = HttpUtil.getUrlContent(AppConfig.IP_API_URL, 5000, httpPort) ?: return null
|
||||||
|
|
||||||
var ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null
|
var ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null
|
||||||
return "(${ipInfo.country_code}) ${ipInfo.ip}"
|
var ip = ipInfo.ip ?: ipInfo.clientIp ?: ipInfo.ip_addr ?: ipInfo.query
|
||||||
|
var country = ipInfo.country_code ?: ipInfo.country ?: ipInfo.countryCode
|
||||||
|
|
||||||
|
return "(${country ?: "unknown"}) $ip"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||||
val bypassLan = SettingsManager.routingRulesetsBypassLan()
|
val bypassLan = SettingsManager.routingRulesetsBypassLan()
|
||||||
if (bypassLan) {
|
if (bypassLan) {
|
||||||
AppConfig.BYPASS_PRIVATE_IP_LIST.forEach {
|
AppConfig.ROUTED_IP_LIST.forEach {
|
||||||
val addr = it.split('/')
|
val addr = it.split('/')
|
||||||
builder.addRoute(addr[0], addr[1].toInt())
|
builder.addRoute(addr[0], addr[1].toInt())
|
||||||
}
|
}
|
||||||
@@ -179,6 +179,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
||||||
if (bypassLan) {
|
if (bypassLan) {
|
||||||
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
|
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
|
||||||
|
builder.addRoute("fc00::", 18) //Xray-core default FakeIPv6 Pool
|
||||||
} else {
|
} else {
|
||||||
builder.addRoute("::", 0)
|
builder.addRoute("::", 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,28 +5,21 @@ import android.content.Intent
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.BuildConfig
|
import com.v2ray.ang.BuildConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityAboutBinding
|
import com.v2ray.ang.databinding.ActivityAboutBinding
|
||||||
import com.v2ray.ang.dto.CheckUpdateResult
|
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.toastError
|
import com.v2ray.ang.extension.toastError
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SpeedtestManager
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
import com.v2ray.ang.handler.UpdateCheckerManager
|
|
||||||
import com.v2ray.ang.util.AppManagerUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.util.ZipUtil
|
import com.v2ray.ang.util.ZipUtil
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -105,23 +98,6 @@ class AboutActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//If it is the Google Play version, not be displayed within 1 days after update
|
|
||||||
// if (Utils.isGoogleFlavor()) {
|
|
||||||
// val lastUpdateTime = AppManagerUtil.getLastUpdateTime(this)
|
|
||||||
// val currentTime = System.currentTimeMillis()
|
|
||||||
// if ((currentTime - lastUpdateTime) < 1 * 24 * 60 * 60 * 1000L) {
|
|
||||||
// binding.layoutCheckUpdate.visibility = View.GONE
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
binding.layoutCheckUpdate.setOnClickListener {
|
|
||||||
checkForUpdates(binding.checkPreRelease.isChecked)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.checkPreRelease.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, isChecked)
|
|
||||||
}
|
|
||||||
binding.checkPreRelease.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, false)
|
|
||||||
|
|
||||||
binding.layoutSoureCcode.setOnClickListener {
|
binding.layoutSoureCcode.setOnClickListener {
|
||||||
Utils.openUri(this, AppConfig.APP_URL)
|
Utils.openUri(this, AppConfig.APP_URL)
|
||||||
}
|
}
|
||||||
@@ -222,28 +198,4 @@ class AboutActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkForUpdates(includePreRelease: Boolean) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
val result = UpdateCheckerManager.checkForUpdate(includePreRelease)
|
|
||||||
if (result.hasUpdate) {
|
|
||||||
showUpdateDialog(result)
|
|
||||||
} else {
|
|
||||||
toast(R.string.update_already_latest_version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showUpdateDialog(result: CheckUpdateResult) {
|
|
||||||
AlertDialog.Builder(this)
|
|
||||||
.setTitle(getString(R.string.update_new_version_found, result.latestVersion))
|
|
||||||
.setMessage(result.releaseNotes)
|
|
||||||
.setPositiveButton(R.string.update_now) { _, _ ->
|
|
||||||
result.downloadUrl?.let {
|
|
||||||
Utils.openUri(this, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.BuildConfig
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.databinding.ActivityCheckUpdateBinding
|
||||||
|
import com.v2ray.ang.dto.CheckUpdateResult
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
|
import com.v2ray.ang.handler.UpdateCheckerManager
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class CheckUpdateActivity : BaseActivity() {
|
||||||
|
|
||||||
|
private val binding by lazy { ActivityCheckUpdateBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
title = getString(R.string.update_check_for_update)
|
||||||
|
|
||||||
|
binding.layoutCheckUpdate.setOnClickListener {
|
||||||
|
checkForUpdates(binding.checkPreRelease.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.checkPreRelease.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, isChecked)
|
||||||
|
}
|
||||||
|
binding.checkPreRelease.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, false)
|
||||||
|
|
||||||
|
"v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
|
||||||
|
binding.tvVersion.text = it
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForUpdates(binding.checkPreRelease.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkForUpdates(includePreRelease: Boolean) {
|
||||||
|
toast(R.string.update_checking_for_update)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val result = UpdateCheckerManager.checkForUpdate(includePreRelease)
|
||||||
|
if (result.hasUpdate) {
|
||||||
|
showUpdateDialog(result)
|
||||||
|
} else {
|
||||||
|
toastSuccess(R.string.update_already_latest_version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showUpdateDialog(result: CheckUpdateResult) {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(getString(R.string.update_new_version_found, result.latestVersion))
|
||||||
|
.setMessage(result.releaseNotes)
|
||||||
|
.setPositiveButton(R.string.update_now) { _, _ ->
|
||||||
|
result.downloadUrl?.let {
|
||||||
|
Utils.openUri(this, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -685,6 +685,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
|
|
||||||
R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.APP_PROMOTION_URL)}?t=${System.currentTimeMillis()}")
|
R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.APP_PROMOTION_URL)}?t=${System.currentTimeMillis()}")
|
||||||
R.id.logcat -> startActivity(Intent(this, LogcatActivity::class.java))
|
R.id.logcat -> startActivity(Intent(this, LogcatActivity::class.java))
|
||||||
|
R.id.check_for_update -> startActivity(Intent(this, CheckUpdateActivity::class.java))
|
||||||
R.id.about -> startActivity(Intent(this, AboutActivity::class.java))
|
R.id.about -> startActivity(Intent(this, AboutActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivitySubEditBinding
|
import com.v2ray.ang.databinding.ActivitySubEditBinding
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
import com.v2ray.ang.dto.SubscriptionItem
|
||||||
@@ -109,19 +110,28 @@ class SubEditActivity : BaseActivity() {
|
|||||||
*/
|
*/
|
||||||
private fun deleteServer(): Boolean {
|
private fun deleteServer(): Boolean {
|
||||||
if (editSubId.isNotEmpty()) {
|
if (editSubId.isNotEmpty()) {
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
MmkvManager.removeSubscription(editSubId)
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
launch(Dispatchers.Main) {
|
MmkvManager.removeSubscription(editSubId)
|
||||||
finish()
|
launch(Dispatchers.Main) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
MmkvManager.removeSubscription(editSubId)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
}
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
@@ -20,6 +21,8 @@ import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
|||||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||||
import com.v2ray.ang.util.QRCodeDecoder
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter {
|
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter {
|
||||||
|
|
||||||
@@ -46,6 +49,10 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
|
||||||
|
removeSubscription(subId, position)
|
||||||
|
}
|
||||||
|
|
||||||
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
||||||
if (!it.isPressed) return@setOnCheckedChangeListener
|
if (!it.isPressed) return@setOnCheckedChangeListener
|
||||||
subItem.enabled = isChecked
|
subItem.enabled = isChecked
|
||||||
@@ -54,9 +61,11 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (TextUtils.isEmpty(subItem.url)) {
|
if (TextUtils.isEmpty(subItem.url)) {
|
||||||
|
holder.itemSubSettingBinding.layoutUrl.visibility = View.GONE
|
||||||
holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE
|
holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE
|
||||||
holder.itemSubSettingBinding.chkEnable.visibility = View.INVISIBLE
|
holder.itemSubSettingBinding.chkEnable.visibility = View.INVISIBLE
|
||||||
} else {
|
} else {
|
||||||
|
holder.itemSubSettingBinding.layoutUrl.visibility = View.VISIBLE
|
||||||
holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE
|
holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE
|
||||||
holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
|
holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
|
||||||
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
|
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
|
||||||
@@ -90,6 +99,32 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun removeSubscription(subId: String, position: Int) {
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||||
|
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
removeSubscriptionSub(subId, position)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
//do noting
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
removeSubscriptionSub(subId, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeSubscriptionSub(subId: String, position: Int) {
|
||||||
|
mActivity.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
MmkvManager.removeSubscription(subId)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
notifyItemRangeChanged(position, mActivity.subscriptions.size)
|
||||||
|
mActivity.refreshData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
||||||
return MainViewHolder(
|
return MainViewHolder(
|
||||||
ItemRecyclerSubSettingBinding.inflate(
|
ItemRecyclerSubSettingBinding.inflate(
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ import java.net.URL
|
|||||||
object HttpUtil {
|
object HttpUtil {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a URL string to its ASCII representation.
|
* Converts the domain part of a URL string to its IDN (Punycode, ASCII Compatible Encoding) format.
|
||||||
*
|
*
|
||||||
* @param str The URL string to convert.
|
* For example, a URL like "https://例子.中国/path" will be converted to "https://xn--fsqu00a.xn--fiqs8s/path".
|
||||||
* @return The ASCII representation of the URL.
|
*
|
||||||
|
* @param str The URL string to convert (can contain non-ASCII characters in the domain).
|
||||||
|
* @return The URL string with the domain part converted to ASCII-compatible (Punycode) format.
|
||||||
*/
|
*/
|
||||||
fun idnToASCII(str: String): String {
|
fun toIdnUrl(str: String): String {
|
||||||
val url = URL(str)
|
val url = URL(str)
|
||||||
val host = url.host
|
val host = url.host
|
||||||
val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED)
|
val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED)
|
||||||
@@ -34,6 +36,28 @@ object HttpUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Unicode domain name to its IDN (Punycode, ASCII Compatible Encoding) format.
|
||||||
|
* If the input is an IP address or already an ASCII domain, returns the original string.
|
||||||
|
*
|
||||||
|
* @param domain The domain string to convert (can include non-ASCII internationalized characters).
|
||||||
|
* @return The domain in ASCII-compatible (Punycode) format, or the original string if input is an IP or already ASCII.
|
||||||
|
*/
|
||||||
|
fun toIdnDomain(domain: String): String {
|
||||||
|
// Return as is if it's a pure IP address (IPv4 or IPv6)
|
||||||
|
if (Utils.isPureIpAddress(domain)) {
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as is if already ASCII (English domain or already punycode)
|
||||||
|
if (domain.all { it.code < 128 }) {
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, convert to ASCII using IDN
|
||||||
|
return IDN.toASCII(domain, IDN.ALLOW_UNASSIGNED)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a hostname to an IP address, returns original input if it's already an IP
|
* Resolves a hostname to an IP address, returns original input if it's already an IP
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -28,13 +28,17 @@ object PluginUtil {
|
|||||||
fun runPlugin(context: Context, config: ProfileItem?, socksPort: Int?) {
|
fun runPlugin(context: Context, config: ProfileItem?, socksPort: Int?) {
|
||||||
Log.i(AppConfig.TAG, "Starting plugin execution")
|
Log.i(AppConfig.TAG, "Starting plugin execution")
|
||||||
|
|
||||||
if (config == null || socksPort == null) {
|
if (config == null) {
|
||||||
Log.w(AppConfig.TAG, "Cannot run plugin: config is null")
|
Log.w(AppConfig.TAG, "Cannot run plugin: config is null")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (config.configType == EConfigType.HYSTERIA2) {
|
if (config.configType == EConfigType.HYSTERIA2) {
|
||||||
|
if (socksPort == null) {
|
||||||
|
Log.w(AppConfig.TAG, "Cannot run plugin: socksPort is null")
|
||||||
|
return
|
||||||
|
}
|
||||||
Log.i(AppConfig.TAG, "Running Hysteria2 plugin")
|
Log.i(AppConfig.TAG, "Running Hysteria2 plugin")
|
||||||
val configFile = genConfigHy2(context, config, socksPort) ?: return
|
val configFile = genConfigHy2(context, config, socksPort) ?: return
|
||||||
val cmd = genCmdHy2(context, configFile)
|
val cmd = genCmdHy2(context, configFile)
|
||||||
|
|||||||
@@ -111,49 +111,6 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingTop="@dimen/padding_spacing_dp16">
|
android:paddingTop="@dimen/padding_spacing_dp16">
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/layout_check_update"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/selectableItemBackground"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
android:gravity="center|start"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:padding="@dimen/padding_spacing_dp16">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="@dimen/image_size_dp24"
|
|
||||||
android:layout_height="@dimen/image_size_dp24"
|
|
||||||
app:srcCompat="@drawable/ic_check_update_24dp" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingStart="@dimen/padding_spacing_dp16">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/update_check_for_update"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.SwitchCompat
|
|
||||||
android:id="@+id/check_pre_release"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/padding_spacing_dp16"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:text="@string/update_check_pre_release"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
|
||||||
android:textColor="@color/colorAccent"
|
|
||||||
app:theme="@style/BrandedSwitch" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/layout_soure_ccode"
|
android:id="@+id/layout_soure_ccode"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
82
V2rayNG/app/src/main/res/layout/activity_check_update.xml
Normal file
82
V2rayNG/app/src/main/res/layout/activity_check_update.xml
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:gravity="center|start"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="@dimen/padding_spacing_dp16">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="@dimen/image_size_dp24"
|
||||||
|
android:layout_height="@dimen/image_size_dp24"
|
||||||
|
app:srcCompat="@drawable/ic_source_code_24dp" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/check_pre_release"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:paddingStart="@dimen/padding_spacing_dp16"
|
||||||
|
android:text="@string/update_check_pre_release"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
|
android:textColor="@color/colorAccent"
|
||||||
|
app:theme="@style/BrandedSwitch" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_check_update"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:gravity="center|start"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="@dimen/padding_spacing_dp16">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="@dimen/image_size_dp24"
|
||||||
|
android:layout_height="@dimen/image_size_dp24"
|
||||||
|
app:srcCompat="@drawable/ic_check_update_24dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="@dimen/padding_spacing_dp16"
|
||||||
|
android:text="@string/update_check_for_update"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="@dimen/padding_spacing_dp16">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_version"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/title_about"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
@@ -15,97 +15,144 @@
|
|||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:nextFocusRight="@+id/layout_edit"
|
android:nextFocusRight="@+id/layout_share"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="@dimen/padding_spacing_dp8">
|
android:padding="@dimen/padding_spacing_dp8">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:orientation="vertical">
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="@dimen/padding_spacing_dp8">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_name"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_url"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
|
||||||
android:lines="2"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="@dimen/padding_spacing_dp8">
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/layout_share"
|
android:layout_width="0dp"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
android:layout_weight="1"
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="@dimen/padding_spacing_dp8">
|
android:paddingStart="@dimen/padding_spacing_dp8">
|
||||||
|
|
||||||
<ImageView
|
<TextView
|
||||||
android:layout_width="@dimen/image_size_dp24"
|
android:id="@+id/tv_name"
|
||||||
android:layout_height="@dimen/image_size_dp24"
|
android:layout_width="wrap_content"
|
||||||
app:srcCompat="@drawable/ic_share_24dp" />
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:minLines="1"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/layout_edit"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:nextFocusLeft="@+id/info_container"
|
android:orientation="horizontal">
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="@dimen/padding_spacing_dp8">
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_share"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:gravity="center"
|
||||||
|
android:nextFocusLeft="@+id/info_container"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="@dimen/padding_spacing_dp8">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="@dimen/image_size_dp24"
|
||||||
|
app:srcCompat="@drawable/ic_share_24dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_edit"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="@dimen/padding_spacing_dp8">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="@dimen/image_size_dp24"
|
||||||
|
android:layout_height="@dimen/image_size_dp24"
|
||||||
|
app:srcCompat="@drawable/ic_edit_24dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_remove"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="@dimen/padding_spacing_dp8">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="@dimen/image_size_dp24"
|
||||||
|
android:layout_height="@dimen/image_size_dp24"
|
||||||
|
app:srcCompat="@drawable/ic_delete_24dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="@dimen/image_size_dp24"
|
|
||||||
android:layout_height="@dimen/image_size_dp24"
|
|
||||||
app:srcCompat="@drawable/ic_edit_24dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/layout_url"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
android:orientation="horizontal"
|
||||||
android:orientation="horizontal">
|
android:paddingStart="@dimen/padding_spacing_dp8"
|
||||||
|
android:paddingEnd="@dimen/padding_spacing_dp8">
|
||||||
|
|
||||||
<androidx.appcompat.widget.SwitchCompat
|
<LinearLayout
|
||||||
android:id="@+id/chk_enable"
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingTop="@dimen/padding_spacing_dp8">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_url"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:lines="2"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:theme="@style/BrandedSwitch" />
|
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/chk_enable"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:theme="@style/BrandedSwitch" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -35,6 +35,10 @@
|
|||||||
android:id="@+id/logcat"
|
android:id="@+id/logcat"
|
||||||
android:icon="@drawable/ic_logcat_24dp"
|
android:icon="@drawable/ic_logcat_24dp"
|
||||||
android:title="@string/title_logcat" />
|
android:title="@string/title_logcat" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/check_for_update"
|
||||||
|
android:icon="@drawable/ic_check_update_24dp"
|
||||||
|
android:title="@string/update_check_for_update" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/about"
|
android:id="@+id/about"
|
||||||
android:icon="@drawable/ic_about_24dp"
|
android:icon="@drawable/ic_about_24dp"
|
||||||
|
|||||||
@@ -316,6 +316,7 @@
|
|||||||
<string name="update_new_version_found">New version found: %s</string>
|
<string name="update_new_version_found">New version found: %s</string>
|
||||||
<string name="update_now">Update now</string>
|
<string name="update_now">Update now</string>
|
||||||
<string name="update_check_pre_release">Check Pre-release</string>
|
<string name="update_check_pre_release">Check Pre-release</string>
|
||||||
|
<string name="update_checking_for_update">Checking for update…</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>رمز استجابة سريعة (QRcode)</item>
|
<item>رمز استجابة سريعة (QRcode)</item>
|
||||||
|
|||||||
@@ -315,6 +315,7 @@
|
|||||||
<string name="update_new_version_found">New version found: %s</string>
|
<string name="update_new_version_found">New version found: %s</string>
|
||||||
<string name="update_now">Update now</string>
|
<string name="update_now">Update now</string>
|
||||||
<string name="update_check_pre_release">Check Pre-release</string>
|
<string name="update_check_pre_release">Check Pre-release</string>
|
||||||
|
<string name="update_checking_for_update">Checking for update…</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>QR কোড</item>
|
<item>QR কোড</item>
|
||||||
|
|||||||
@@ -148,7 +148,7 @@
|
|||||||
|
|
||||||
<string name="title_mux_settings">سامووا Mux</string>
|
<string name="title_mux_settings">سامووا Mux</string>
|
||||||
<string name="title_pref_mux_enabled">ر وندن Mux</string>
|
<string name="title_pref_mux_enabled">ر وندن Mux</string>
|
||||||
<string name="summary_pref_mux_enabled">زل تر، ٱما گاشڌ منپیز زی قت بۊ بارت دؽوۉداری، TCP، UDP و QUIC ن ای لم سفارشی کۊنین.</string>
|
<string name="summary_pref_mux_enabled">زل تر، ٱما گاشڌ منپیز زی قت بۊ\nمخزن ترافیک TCP وا 8 منپیز پؽش فرز، بارت دؽوۉداری UDP وو QUIC ن ای لم سفارشی کۊنین.</string>
|
||||||
<string name="title_pref_mux_concurency">منپیزا TCP (تلایه منجا 1-1024)</string>
|
<string name="title_pref_mux_concurency">منپیزا TCP (تلایه منجا 1-1024)</string>
|
||||||
<string name="title_pref_mux_xudp_concurency">منپیزا XUDP (تلایه منجا 1-1024)</string>
|
<string name="title_pref_mux_xudp_concurency">منپیزا XUDP (تلایه منجا 1-1024)</string>
|
||||||
<string name="title_pref_mux_xudp_quic">دؽوۉداری QUIC من تۊنل mux</string>
|
<string name="title_pref_mux_xudp_quic">دؽوۉداری QUIC من تۊنل mux</string>
|
||||||
@@ -324,6 +324,7 @@
|
|||||||
<string name="update_new_version_found">نوسخه نۊ ن جوست: %s</string>
|
<string name="update_new_version_found">نوسخه نۊ ن جوست: %s</string>
|
||||||
<string name="update_now">سکو ورۊ رسۊوی کۊنین</string>
|
<string name="update_now">سکو ورۊ رسۊوی کۊنین</string>
|
||||||
<string name="update_check_pre_release">واجۊری نوسخه یل پؽش ز تیجنیڌن</string>
|
<string name="update_check_pre_release">واجۊری نوسخه یل پؽش ز تیجنیڌن</string>
|
||||||
|
<string name="update_checking_for_update">ورۊ رسۊوی ن هونی واجۊری اکونه...</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>QRcode</item>
|
<item>QRcode</item>
|
||||||
|
|||||||
@@ -321,6 +321,7 @@
|
|||||||
<string name="update_new_version_found">نسخه جدید پیدا شد: %s</string>
|
<string name="update_new_version_found">نسخه جدید پیدا شد: %s</string>
|
||||||
<string name="update_now">اکنون به روز رسانی کنید</string>
|
<string name="update_now">اکنون به روز رسانی کنید</string>
|
||||||
<string name="update_check_pre_release">بررسی نسخه پیش از انتشار</string>
|
<string name="update_check_pre_release">بررسی نسخه پیش از انتشار</string>
|
||||||
|
<string name="update_checking_for_update">در حال بررسی برای بهروزرسانی…</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>QRcode</item>
|
<item>QRcode</item>
|
||||||
|
|||||||
@@ -323,6 +323,7 @@
|
|||||||
<string name="update_new_version_found">Найдена новая версия: %s</string>
|
<string name="update_new_version_found">Найдена новая версия: %s</string>
|
||||||
<string name="update_now">Обновить</string>
|
<string name="update_now">Обновить</string>
|
||||||
<string name="update_check_pre_release">Искать предварительный выпуск</string>
|
<string name="update_check_pre_release">Искать предварительный выпуск</string>
|
||||||
|
<string name="update_checking_for_update">Проверка обновления…</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>QR-код</item>
|
<item>QR-код</item>
|
||||||
|
|||||||
@@ -317,6 +317,7 @@
|
|||||||
<string name="update_new_version_found">New version found: %s</string>
|
<string name="update_new_version_found">New version found: %s</string>
|
||||||
<string name="update_now">Update now</string>
|
<string name="update_now">Update now</string>
|
||||||
<string name="update_check_pre_release">Check Pre-release</string>
|
<string name="update_check_pre_release">Check Pre-release</string>
|
||||||
|
<string name="update_checking_for_update">Checking for update…</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>Xuất ra mã QR (Chụp màn hình để lưu)</item>
|
<item>Xuất ra mã QR (Chụp màn hình để lưu)</item>
|
||||||
|
|||||||
@@ -315,6 +315,7 @@
|
|||||||
<string name="update_new_version_found">发现新版本: %s</string>
|
<string name="update_new_version_found">发现新版本: %s</string>
|
||||||
<string name="update_now">立即更新</string>
|
<string name="update_now">立即更新</string>
|
||||||
<string name="update_check_pre_release">检查 Pre-release</string>
|
<string name="update_check_pre_release">检查 Pre-release</string>
|
||||||
|
<string name="update_checking_for_update">正在检查更新中…</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>二维码</item>
|
<item>二维码</item>
|
||||||
|
|||||||
@@ -315,6 +315,7 @@
|
|||||||
<string name="update_new_version_found">發現新版本: %s</string>
|
<string name="update_new_version_found">發現新版本: %s</string>
|
||||||
<string name="update_now">立即更新</string>
|
<string name="update_now">立即更新</string>
|
||||||
<string name="update_check_pre_release">檢查 Pre-release</string>
|
<string name="update_check_pre_release">檢查 Pre-release</string>
|
||||||
|
<string name="update_checking_for_update">正在檢查更新中…</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>QR Code</item>
|
<item>QR Code</item>
|
||||||
|
|||||||
@@ -325,6 +325,7 @@
|
|||||||
<string name="update_new_version_found">New version found: %s</string>
|
<string name="update_new_version_found">New version found: %s</string>
|
||||||
<string name="update_now">Update now</string>
|
<string name="update_now">Update now</string>
|
||||||
<string name="update_check_pre_release">Check Pre-release</string>
|
<string name="update_check_pre_release">Check Pre-release</string>
|
||||||
|
<string name="update_checking_for_update">Checking for update…</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>QRcode</item>
|
<item>QRcode</item>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
android:title="@string/title_pref_vpn_dns" />
|
android:title="@string/title_pref_vpn_dns" />
|
||||||
|
|
||||||
<ListPreference
|
<ListPreference
|
||||||
android:defaultValue="0"
|
android:defaultValue="1"
|
||||||
android:entries="@array/vpn_bypass_lan"
|
android:entries="@array/vpn_bypass_lan"
|
||||||
android:entryValues="@array/vpn_bypass_lan_value"
|
android:entryValues="@array/vpn_bypass_lan_value"
|
||||||
android:key="pref_vpn_bypass_lan"
|
android:key="pref_vpn_bypass_lan"
|
||||||
|
|||||||
@@ -10,31 +10,31 @@ class HttpUtilTest {
|
|||||||
fun testIdnToASCII() {
|
fun testIdnToASCII() {
|
||||||
// Regular URL remains unchanged
|
// Regular URL remains unchanged
|
||||||
val regularUrl = "https://example.com/path"
|
val regularUrl = "https://example.com/path"
|
||||||
assertEquals(regularUrl, HttpUtil.idnToASCII(regularUrl))
|
assertEquals(regularUrl, HttpUtil.toIdnUrl(regularUrl))
|
||||||
|
|
||||||
// Non-ASCII URL converts to ASCII (Punycode)
|
// Non-ASCII URL converts to ASCII (Punycode)
|
||||||
val nonAsciiUrl = "https://例子.测试/path"
|
val nonAsciiUrl = "https://例子.测试/path"
|
||||||
val expectedNonAscii = "https://xn--fsqu00a.xn--0zwm56d/path"
|
val expectedNonAscii = "https://xn--fsqu00a.xn--0zwm56d/path"
|
||||||
assertEquals(expectedNonAscii, HttpUtil.idnToASCII(nonAsciiUrl))
|
assertEquals(expectedNonAscii, HttpUtil.toIdnUrl(nonAsciiUrl))
|
||||||
|
|
||||||
// Mixed URL only converts the host part
|
// Mixed URL only converts the host part
|
||||||
val mixedUrl = "https://例子.com/测试"
|
val mixedUrl = "https://例子.com/测试"
|
||||||
val expectedMixed = "https://xn--fsqu00a.com/测试"
|
val expectedMixed = "https://xn--fsqu00a.com/测试"
|
||||||
assertEquals(expectedMixed, HttpUtil.idnToASCII(mixedUrl))
|
assertEquals(expectedMixed, HttpUtil.toIdnUrl(mixedUrl))
|
||||||
|
|
||||||
// URL with Basic Authentication using regular domain
|
// URL with Basic Authentication using regular domain
|
||||||
val basicAuthUrl = "https://user:password@example.com/path"
|
val basicAuthUrl = "https://user:password@example.com/path"
|
||||||
assertEquals(basicAuthUrl, HttpUtil.idnToASCII(basicAuthUrl))
|
assertEquals(basicAuthUrl, HttpUtil.toIdnUrl(basicAuthUrl))
|
||||||
|
|
||||||
// URL with Basic Authentication using non-ASCII domain
|
// URL with Basic Authentication using non-ASCII domain
|
||||||
val basicAuthNonAscii = "https://user:password@例子.测试/path"
|
val basicAuthNonAscii = "https://user:password@例子.测试/path"
|
||||||
val expectedBasicAuthNonAscii = "https://user:password@xn--fsqu00a.xn--0zwm56d/path"
|
val expectedBasicAuthNonAscii = "https://user:password@xn--fsqu00a.xn--0zwm56d/path"
|
||||||
assertEquals(expectedBasicAuthNonAscii, HttpUtil.idnToASCII(basicAuthNonAscii))
|
assertEquals(expectedBasicAuthNonAscii, HttpUtil.toIdnUrl(basicAuthNonAscii))
|
||||||
|
|
||||||
// URL with non-ASCII username and password
|
// URL with non-ASCII username and password
|
||||||
val nonAsciiAuth = "https://用户:密码@example.com/path"
|
val nonAsciiAuth = "https://用户:密码@example.com/path"
|
||||||
// Basic auth credentials should remain unchanged as they're percent-encoded separately
|
// Basic auth credentials should remain unchanged as they're percent-encoded separately
|
||||||
assertEquals(nonAsciiAuth, HttpUtil.idnToASCII(nonAsciiAuth))
|
assertEquals(nonAsciiAuth, HttpUtil.toIdnUrl(nonAsciiAuth))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.9.2"
|
agp = "8.10.1"
|
||||||
desugarJdkLibs = "2.1.5"
|
desugarJdkLibs = "2.1.5"
|
||||||
gradleLicensePlugin = "0.9.8"
|
gradleLicensePlugin = "0.9.8"
|
||||||
kotlin = "2.1.20"
|
kotlin = "2.1.21"
|
||||||
coreKtx = "1.16.0"
|
coreKtx = "1.16.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.2.1"
|
junitVersion = "1.2.1"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
appcompat = "1.7.0"
|
appcompat = "1.7.1"
|
||||||
material = "1.12.0"
|
material = "1.12.0"
|
||||||
activity = "1.10.1"
|
activity = "1.10.1"
|
||||||
constraintlayout = "2.2.1"
|
constraintlayout = "2.2.1"
|
||||||
@@ -21,7 +21,7 @@ toasty = "1.5.2"
|
|||||||
editorkit = "2.9.0"
|
editorkit = "2.9.0"
|
||||||
core = "3.5.3"
|
core = "3.5.3"
|
||||||
workRuntimeKtx = "2.10.1"
|
workRuntimeKtx = "2.10.1"
|
||||||
lifecycleViewmodelKtx = "2.8.7"
|
lifecycleViewmodelKtx = "2.9.1"
|
||||||
multidex = "2.0.1"
|
multidex = "2.0.1"
|
||||||
mockitoMockitoInline = "5.2.0"
|
mockitoMockitoInline = "5.2.0"
|
||||||
flexbox = "3.0.0"
|
flexbox = "3.0.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#Thu Nov 14 12:42:51 BDT 2024
|
#Thu Nov 14 12:42:51 BDT 2024
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
Reference in New Issue
Block a user