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