Compare commits

..

28 Commits

Author SHA1 Message Date
2dust 94cc72d2b9 up 1.10.7 2025-06-19 14:40:47 +08:00
2dust f68c353715 Update AndroidLibXrayLite 2025-06-19 14:40:11 +08:00
2dust e077c18108 Improved update checking and prompts in case of abnormality 2025-06-19 14:40:07 +08:00
Ural Khamitov 1a5e105212 Fix blinking QSTile when QS panel is opening (#4676) 2025-06-18 16:17:28 +08:00
DHR60 e0881caab4 Fix missing sockopt.domainStrategy (#4673)
* Fix missing sockopt.domainStrategy

* Fix
2025-06-17 13:43:03 +08:00
DHR60 7219425258 Cloudflare DNS Hosts (#4661) 2025-06-15 09:46:30 +08:00
2dust 51eabe5440 up 1.10.6 2025-06-14 14:27:09 +08:00
2dust 6f0b3ce990 Update AndroidLibXrayLite 2025-06-14 14:26:37 +08:00
2dust 69e27ed3bb Fix log for plugin 2025-06-14 14:26:33 +08:00
patterniha fff6ab30e6 Xray-core default FakeIPv6 Pool should not bypass and should route (#4649)
* Update V2RayVpnService.kt

* Update V2RayVpnService.kt

* Update AppConfig.kt
2025-06-14 13:59:59 +08:00
2dust fdb67a86f4 up 1.10.5 2025-06-08 09:26:36 +08:00
2dust ea088376ac Update AndroidLibXrayLite 2025-06-08 09:25:46 +08:00
2dust 52332d960e Update libs.versions.toml 2025-06-07 11:20:41 +08:00
2dust 3ead542e2b VPN bypass LAN By default 2025-06-07 11:20:37 +08:00
2dust 9d1f98ff34 Fix non-English domain
https://github.com/2dust/v2rayNG/issues/4626
https://github.com/2dust/v2rayNG/commit/f305e26a395650301e5565d95ffb1d3199e846ed
2025-05-31 14:03:52 +08:00
2dust f305e26a39 Fix the parsing problem of non-English domain
https://github.com/2dust/v2rayNG/issues/4626
2025-05-31 11:12:57 +08:00
2dust aa47fba20d up 1.10.4 2025-05-25 11:06:15 +08:00
Hossein Abaspanah 69c5bbfd3d Improved Luri Bakhtiari Translation (#4600) 2025-05-25 10:12:52 +08:00
Pk-web6936 90ed02804c Update Persian translate (#4607) 2025-05-25 10:12:45 +08:00
Hossein Abaspanah 822c1de79c Update Luri Bakhtiari translation (#4610) 2025-05-25 10:12:39 +08:00
solokot d910b93525 Update Russian translation (#4611) 2025-05-25 10:12:29 +08:00
Pk-web6936 7e6b1c247b Update kotlin version to 2.1.21 (#4583)
* Update kotlin version to 2.1.21

* Update kotlin version to 2.1.21
2025-05-23 16:58:03 +08:00
2dust f3f2b7fab5 Added delete function to subscription group list, secondary confirmation with settings 2025-05-23 16:17:38 +08:00
2dust e6f260da76 Added the check update entry to the main interface drawer menu
https://github.com/2dust/v2rayNG/issues/4599
2025-05-23 14:34:55 +08:00
2dust 55bc2bf934 up 1.10.3 2025-05-17 12:01:34 +08:00
2dust f22454da5d Update AndroidLibXrayLite 2025-05-17 11:48:15 +08:00
2dust 4a87549fa7 Update README.md 2025-05-15 10:58:52 +08:00
2dust d447adc97f Fix
https://github.com/2dust/v2rayN/discussions/7268
2025-05-11 18:07:26 +08:00
44 changed files with 450 additions and 235 deletions
+1 -5
View File
@@ -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)
[![API](https://img.shields.io/badge/API-21%2B-yellow.svg?style=flat)](https://developer.android.com/about/versions/lollipop)
[![Kotlin Version](https://img.shields.io/badge/Kotlin-2.1.20-blue.svg)](https://kotlinlang.org)
[![Kotlin Version](https://img.shields.io/badge/Kotlin-2.1.21-blue.svg)](https://kotlinlang.org)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayNG)](https://github.com/2dust/v2rayNG/commits/master)
[![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayng/badge)](https://www.codefactor.io/repository/github/2dust/v2rayng)
[![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayNG/latest/total?logo=github)](https://github.com/2dust/v2rayNG/releases)
[![Chat on Telegram](https://img.shields.io/badge/Chat%20on-Telegram-brightgreen.svg)](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
[github_2dust](https://t.me/github_2dust)
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang"
minSdk = 21
targetSdk = 35
versionCode = 652
versionName = "1.10.2"
versionCode = 657
versionName = "1.10.7"
multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
+3
View File
@@ -144,6 +144,9 @@
<data android:host="install-sub" />
</intent-filter>
</activity>
<activity
android:name=".ui.CheckUpdateActivity"
android:exported="false" />
<activity
android:name=".ui.AboutActivity"
android:exported="false" />
@@ -103,7 +103,7 @@ object AppConfig {
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_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. */
const val DNS_PROXY = "1.1.1.1"
@@ -168,7 +168,9 @@ object AppConfig {
// Android Private DNS constants
const val DNS_DNSPOD_DOMAIN = "dot.pub"
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
const val DNS_CLOUDFLARE_DOMAIN = "one.one.one.one"
const val DNS_CLOUDFLARE_ONE_DOMAIN = "one.one.one.one"
const val DNS_CLOUDFLARE_DNS_COM_DOMAIN = "dns.cloudflare.com"
const val DNS_CLOUDFLARE_DNS_DOMAIN = "cloudflare-dns.com"
const val DNS_GOOGLE_DOMAIN = "dns.google"
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
@@ -182,14 +184,16 @@ object AppConfig {
const val HEADER_TYPE_HTTP = "http"
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
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_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_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_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
val BYPASS_PRIVATE_IP_LIST = arrayListOf(
val ROUTED_IP_LIST = arrayListOf(
"0.0.0.0/5",
"8.0.0.0/7",
"11.0.0.0/8",
@@ -2,10 +2,11 @@ package com.v2ray.ang.dto
data class IPAPIInfo(
var ip: String? = null,
var city: String? = null,
var region: String? = null,
var region_code: String? = null,
var clientIp: String? = null,
var ip_addr: String? = null,
var query: String? = null,
var country: 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.ProfileItem
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -26,7 +27,7 @@ open class FmtBase {
val url = String.format(
"%s@%s:%s",
Utils.urlEncode(userInfo ?: ""),
Utils.getIpv6Address(config.server),
Utils.getIpv6Address(HttpUtil.toIdnDomain(config.server.orEmpty())),
config.serverPort
)
@@ -149,6 +150,8 @@ open class FmtBase {
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)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.address = getServerAddress(profileItem)
server.port = profileItem.serverPort.orEmpty().toInt()
if (profileItem.username.isNotNullEmpty()) {
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
@@ -135,7 +135,7 @@ object ShadowsocksFmt : FmtBase() {
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SHADOWSOCKS)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.address = getServerAddress(profileItem)
server.port = profileItem.serverPort.orEmpty().toInt()
server.password = profileItem.password
server.method = profileItem.method
@@ -64,7 +64,7 @@ object SocksFmt : FmtBase() {
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SOCKS)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.address = getServerAddress(profileItem)
server.port = profileItem.serverPort.orEmpty().toInt()
if (profileItem.username.isNotNullEmpty()) {
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
@@ -64,7 +64,7 @@ object TrojanFmt : FmtBase() {
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.TROJAN)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.address = getServerAddress(profileItem)
server.port = profileItem.serverPort.orEmpty().toInt()
server.password = profileItem.password
server.flow = profileItem.flow
@@ -60,7 +60,7 @@ object VlessFmt : FmtBase() {
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VLESS)
outboundBean?.settings?.vnext?.first()?.let { vnext ->
vnext.address = profileItem.server.orEmpty()
vnext.address = getServerAddress(profileItem)
vnext.port = profileItem.serverPort.orEmpty().toInt()
vnext.users[0].id = profileItem.password.orEmpty()
vnext.users[0].encryption = profileItem.method
@@ -172,7 +172,7 @@ object VmessFmt : FmtBase() {
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VMESS)
outboundBean?.settings?.vnext?.first()?.let { vnext ->
vnext.address = profileItem.server.orEmpty()
vnext.address = getServerAddress(profileItem)
vnext.port = profileItem.serverPort.orEmpty().toInt()
vnext.users[0].id = profileItem.password.orEmpty()
vnext.users[0].security = profileItem.method
@@ -415,7 +415,7 @@ object AngConfigManager {
if (!it.second.enabled) {
return 0
}
val url = HttpUtil.idnToASCII(it.second.url)
val url = HttpUtil.toIdnUrl(it.second.url)
if (!Utils.isValidUrl(url)) {
return 0
}
@@ -159,7 +159,7 @@ object SettingsManager {
* @return True if bypassing LAN, false otherwise.
*/
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") {
return true
} else if (vpnBypassLan == "2") {
@@ -168,10 +168,13 @@ object SpeedtestManager {
fun getRemoteIPInfo(): String? {
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
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"
}
/**
@@ -17,7 +17,6 @@ import java.io.FileOutputStream
object UpdateCheckerManager {
suspend fun checkForUpdate(includePreRelease: Boolean = false): CheckUpdateResult = withContext(Dispatchers.IO) {
try {
val url = if (includePreRelease) {
AppConfig.APP_API_URL
} else {
@@ -53,10 +52,6 @@ object UpdateCheckerManager {
} else {
CheckUpdateResult(hasUpdate = false)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}")
return@withContext CheckUpdateResult(hasUpdate = false, error = e.message)
}
}
suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) {
@@ -16,7 +16,6 @@ import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.fmt.HttpFmt
import com.v2ray.ang.fmt.Hysteria2Fmt
import com.v2ray.ang.fmt.ShadowsocksFmt
import com.v2ray.ang.fmt.SocksFmt
import com.v2ray.ang.fmt.TrojanFmt
@@ -479,6 +478,25 @@ object V2rayConfigManager {
)
}
//block dns
val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED)
if (blkDomain.isNotEmpty()) {
hosts.putAll(blkDomain.map { it to AppConfig.LOOPBACK })
}
// hardcode googleapi rule to fix play store problems
hosts[AppConfig.GOOGLEAPIS_CN_DOMAIN] = AppConfig.GOOGLEAPIS_COM_DOMAIN
// hardcode popular Android Private DNS rule to fix localhost DNS problem
hosts[AppConfig.DNS_ALIDNS_DOMAIN] = AppConfig.DNS_ALIDNS_ADDRESSES
hosts[AppConfig.DNS_CLOUDFLARE_ONE_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ONE_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_GOOGLE_DOMAIN] = AppConfig.DNS_GOOGLE_ADDRESSES
hosts[AppConfig.DNS_QUAD9_DOMAIN] = AppConfig.DNS_QUAD9_ADDRESSES
hosts[AppConfig.DNS_YANDEX_DOMAIN] = AppConfig.DNS_YANDEX_ADDRESSES
//User DNS hosts
try {
val userHosts = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
@@ -493,24 +511,6 @@ object V2rayConfigManager {
Log.e(AppConfig.TAG, "Failed to configure user DNS hosts", e)
}
//block dns
val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED)
if (blkDomain.isNotEmpty()) {
hosts.putAll(blkDomain.map { it to AppConfig.LOOPBACK })
}
// hardcode googleapi rule to fix play store problems
hosts[AppConfig.GOOGLEAPIS_CN_DOMAIN] = AppConfig.GOOGLEAPIS_COM_DOMAIN
// hardcode popular Android Private DNS rule to fix localhost DNS problem
hosts[AppConfig.DNS_ALIDNS_DOMAIN] = AppConfig.DNS_ALIDNS_ADDRESSES
hosts[AppConfig.DNS_CLOUDFLARE_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ADDRESSES
hosts[AppConfig.DNS_DNSPOD_DOMAIN] = AppConfig.DNS_DNSPOD_ADDRESSES
hosts[AppConfig.DNS_GOOGLE_DOMAIN] = AppConfig.DNS_GOOGLE_ADDRESSES
hosts[AppConfig.DNS_QUAD9_DOMAIN] = AppConfig.DNS_QUAD9_ADDRESSES
hosts[AppConfig.DNS_YANDEX_DOMAIN] = AppConfig.DNS_YANDEX_ADDRESSES
// DNS dns
v2rayConfig.dns = V2rayConfig.DnsBean(
servers = servers,
@@ -828,7 +828,11 @@ object V2rayConfigManager {
for (item in proxyOutboundList) {
val domain = item.getServerAddress()
if (domain.isNullOrEmpty()) continue
if (newHosts.containsKey(domain)) continue
if (newHosts.containsKey(domain)) {
item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6"
continue
}
val resolvedIps = HttpUtil.resolveHostToIP(domain, preferIpv6)
if (resolvedIps.isNullOrEmpty()) continue
@@ -25,14 +25,13 @@ class QSTileService : TileService() {
* @param state The state to set.
*/
fun setState(state: Int) {
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
if (state == Tile.STATE_INACTIVE) {
qsTile?.state = Tile.STATE_INACTIVE
qsTile?.label = getString(R.string.app_name)
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
} else if (state == Tile.STATE_ACTIVE) {
qsTile?.state = Tile.STATE_ACTIVE
qsTile?.label = V2RayServiceManager.getRunningServerName()
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
}
qsTile?.updateTile()
@@ -45,7 +44,11 @@ class QSTileService : TileService() {
override fun onStartListening() {
super.onStartListening()
setState(Tile.STATE_INACTIVE)
if (V2RayServiceManager.isRunning()) {
setState(Tile.STATE_ACTIVE)
} else {
setState(Tile.STATE_INACTIVE)
}
mMsgReceive = ReceiveMessageHandler(this)
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
ContextCompat.registerReceiver(applicationContext, mMsgReceive, mFilter, Utils.receiverFlags())
@@ -167,7 +167,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
val bypassLan = SettingsManager.routingRulesetsBypassLan()
if (bypassLan) {
AppConfig.BYPASS_PRIVATE_IP_LIST.forEach {
AppConfig.ROUTED_IP_LIST.forEach {
val addr = it.split('/')
builder.addRoute(addr[0], addr[1].toInt())
}
@@ -179,6 +179,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
if (bypassLan) {
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
builder.addRoute("fc00::", 18) //Xray-core default FakeIPv6 Pool
} else {
builder.addRoute("::", 0)
}
@@ -5,28 +5,21 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityAboutBinding
import com.v2ray.ang.dto.CheckUpdateResult
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
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.AppManagerUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.ZipUtil
import kotlinx.coroutines.launch
import java.io.File
import java.text.SimpleDateFormat
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 {
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,77 @@
package com.v2ray.ang.ui
import android.os.Bundle
import android.util.Log
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.toastError
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 {
try {
val result = UpdateCheckerManager.checkForUpdate(includePreRelease)
if (result.hasUpdate) {
showUpdateDialog(result)
} else {
toastSuccess(R.string.update_already_latest_version)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}")
toastError(e.message ?: getString(R.string.toast_failure))
}
}
}
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.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))
}
@@ -6,6 +6,7 @@ import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubEditBinding
import com.v2ray.ang.dto.SubscriptionItem
@@ -109,19 +110,28 @@ class SubEditActivity : BaseActivity() {
*/
private fun deleteServer(): Boolean {
if (editSubId.isNotEmpty()) {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch(Dispatchers.IO) {
MmkvManager.removeSubscription(editSubId)
launch(Dispatchers.Main) {
finish()
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch(Dispatchers.IO) {
MmkvManager.removeSubscription(editSubId)
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
}
@@ -8,6 +8,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.AppConfig
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.util.QRCodeDecoder
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
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 ->
if (!it.isPressed) return@setOnCheckedChangeListener
subItem.enabled = isChecked
@@ -54,9 +61,11 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
}
if (TextUtils.isEmpty(subItem.url)) {
holder.itemSubSettingBinding.layoutUrl.visibility = View.GONE
holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE
holder.itemSubSettingBinding.chkEnable.visibility = View.INVISIBLE
} else {
holder.itemSubSettingBinding.layoutUrl.visibility = View.VISIBLE
holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE
holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
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 {
return MainViewHolder(
ItemRecyclerSubSettingBinding.inflate(
@@ -18,12 +18,14 @@ import java.net.URL
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.
* @return The ASCII representation of the URL.
* For example, a URL like "https://例子.中国/path" will be converted to "https://xn--fsqu00a.xn--fiqs8s/path".
*
* @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 host = url.host
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
*
@@ -28,13 +28,17 @@ object PluginUtil {
fun runPlugin(context: Context, config: ProfileItem?, socksPort: Int?) {
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")
return
}
try {
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")
val configFile = genConfigHy2(context, config, socksPort) ?: return
val cmd = genCmdHy2(context, configFile)
@@ -111,49 +111,6 @@
android:orientation="vertical"
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
android:id="@+id/layout_soure_ccode"
android:layout_width="match_parent"
@@ -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:focusable="true"
android:gravity="center"
android:nextFocusRight="@+id/layout_edit"
android:nextFocusRight="@+id/layout_share"
android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp8">
<LinearLayout
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
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">
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/layout_share"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:layout_weight="1"
android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp8">
android:paddingStart="@dimen/padding_spacing_dp8">
<ImageView
android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24"
app:srcCompat="@drawable/ic_share_24dp" />
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:minLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</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:nextFocusLeft="@+id/info_container"
android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp8">
android:orientation="horizontal">
<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
android:layout_width="wrap_content"
android:id="@+id/layout_url"
android:layout_width="match_parent"
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
android:id="@+id/chk_enable"
<LinearLayout
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_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>
@@ -35,6 +35,10 @@
android:id="@+id/logcat"
android:icon="@drawable/ic_logcat_24dp"
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
android:id="@+id/about"
android:icon="@drawable/ic_about_24dp"
@@ -316,6 +316,7 @@
<string name="update_new_version_found">New version found: %s</string>
<string name="update_now">Update now</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">
<item>رمز استجابة سريعة (QRcode)</item>
@@ -315,6 +315,7 @@
<string name="update_new_version_found">New version found: %s</string>
<string name="update_now">Update now</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">
<item>QR কোড</item>
@@ -148,7 +148,7 @@
<string name="title_mux_settings">سامووا 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_xudp_concurency">منپیزا XUDP (تلایه منجا 1-1024)</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_now">سکو ورۊ رسۊوی کۊنین</string>
<string name="update_check_pre_release">واجۊری نوسخه یل پؽش ز تیجنیڌن</string>
<string name="update_checking_for_update">ورۊ رسۊوی ن هونی واجۊری اکونه...</string>
<string-array name="share_method">
<item>QRcode</item>
@@ -321,6 +321,7 @@
<string name="update_new_version_found">نسخه جدید پیدا شد: %s</string>
<string name="update_now">اکنون به روز رسانی کنید</string>
<string name="update_check_pre_release">بررسی نسخه پیش از انتشار</string>
<string name="update_checking_for_update">در حال بررسی برای به‌روزرسانی…</string>
<string-array name="share_method">
<item>QRcode</item>
@@ -323,6 +323,7 @@
<string name="update_new_version_found">Найдена новая версия: %s</string>
<string name="update_now">Обновить</string>
<string name="update_check_pre_release">Искать предварительный выпуск</string>
<string name="update_checking_for_update">Проверка обновления…</string>
<string-array name="share_method">
<item>QR-код</item>
@@ -317,6 +317,7 @@
<string name="update_new_version_found">New version found: %s</string>
<string name="update_now">Update now</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">
<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_now">立即更新</string>
<string name="update_check_pre_release">检查 Pre-release</string>
<string name="update_checking_for_update">正在检查更新中…</string>
<string-array name="share_method">
<item>二维码</item>
@@ -315,6 +315,7 @@
<string name="update_new_version_found">發現新版本: %s</string>
<string name="update_now">立即更新</string>
<string name="update_check_pre_release">檢查 Pre-release</string>
<string name="update_checking_for_update">正在檢查更新中…</string>
<string-array name="share_method">
<item>QR Code</item>
@@ -325,6 +325,7 @@
<string name="update_new_version_found">New version found: %s</string>
<string name="update_now">Update now</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">
<item>QRcode</item>
@@ -56,7 +56,7 @@
android:title="@string/title_pref_vpn_dns" />
<ListPreference
android:defaultValue="0"
android:defaultValue="1"
android:entries="@array/vpn_bypass_lan"
android:entryValues="@array/vpn_bypass_lan_value"
android:key="pref_vpn_bypass_lan"
@@ -10,31 +10,31 @@ class HttpUtilTest {
fun testIdnToASCII() {
// Regular URL remains unchanged
val regularUrl = "https://example.com/path"
assertEquals(regularUrl, HttpUtil.idnToASCII(regularUrl))
assertEquals(regularUrl, HttpUtil.toIdnUrl(regularUrl))
// Non-ASCII URL converts to ASCII (Punycode)
val nonAsciiUrl = "https://例子.测试/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
val mixedUrl = "https://例子.com/测试"
val expectedMixed = "https://xn--fsqu00a.com/测试"
assertEquals(expectedMixed, HttpUtil.idnToASCII(mixedUrl))
assertEquals(expectedMixed, HttpUtil.toIdnUrl(mixedUrl))
// URL with Basic Authentication using regular domain
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
val basicAuthNonAscii = "https://user:password@例子.测试/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
val nonAsciiAuth = "https://用户:密码@example.com/path"
// Basic auth credentials should remain unchanged as they're percent-encoded separately
assertEquals(nonAsciiAuth, HttpUtil.idnToASCII(nonAsciiAuth))
assertEquals(nonAsciiAuth, HttpUtil.toIdnUrl(nonAsciiAuth))
}
+4 -4
View File
@@ -1,13 +1,13 @@
[versions]
agp = "8.9.2"
agp = "8.10.1"
desugarJdkLibs = "2.1.5"
gradleLicensePlugin = "0.9.8"
kotlin = "2.1.20"
kotlin = "2.1.21"
coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
appcompat = "1.7.0"
appcompat = "1.7.1"
material = "1.12.0"
activity = "1.10.1"
constraintlayout = "2.2.1"
@@ -21,7 +21,7 @@ toasty = "1.5.2"
editorkit = "2.9.0"
core = "3.5.3"
workRuntimeKtx = "2.10.1"
lifecycleViewmodelKtx = "2.8.7"
lifecycleViewmodelKtx = "2.9.1"
multidex = "2.0.1"
mockitoMockitoInline = "5.2.0"
flexbox = "3.0.0"
+1 -1
View File
@@ -1,6 +1,6 @@
#Thu Nov 14 12:42:51 BDT 2024
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists