Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39d2513df1 | |||
| 3cb4df474b | |||
| b64bb92c8c | |||
| 7f0e2f801d | |||
| 0f7cee0613 | |||
| b2e10fc60b | |||
| aa3f17fb51 |
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 37
|
targetSdk = 37
|
||||||
versionCode = 730
|
versionCode = 731
|
||||||
versionName = "2.2.0"
|
versionName = "2.2.1"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.v2ray.ang.dto.CoreConfigContext
|
|||||||
import com.v2ray.ang.dto.entities.ProfileItem
|
import com.v2ray.ang.dto.entities.ProfileItem
|
||||||
import com.v2ray.ang.enums.CoreResolvedType
|
import com.v2ray.ang.enums.CoreResolvedType
|
||||||
import com.v2ray.ang.enums.EConfigType
|
import com.v2ray.ang.enums.EConfigType
|
||||||
|
import com.v2ray.ang.extension.isComplexType
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
@@ -163,9 +164,7 @@ object CoreConfigContextBuilder {
|
|||||||
}
|
}
|
||||||
.filter { it.server.isNotNullEmpty() }
|
.filter { it.server.isNotNullEmpty() }
|
||||||
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
||||||
.filter { it.configType != EConfigType.CUSTOM }
|
.filter { !it.configType.isComplexType() }
|
||||||
.filter { it.configType != EConfigType.POLICYGROUP }
|
|
||||||
.filter { it.configType != EConfigType.PROXYCHAIN }
|
|
||||||
.toList()
|
.toList()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to resolve policy group profiles for '${config.remarks}'", e)
|
LogUtil.e(AppConfig.TAG, "Failed to resolve policy group profiles for '${config.remarks}'", e)
|
||||||
@@ -184,9 +183,7 @@ object CoreConfigContextBuilder {
|
|||||||
.mapNotNull { remark -> SettingsManager.getServerViaRemarks(remark) }
|
.mapNotNull { remark -> SettingsManager.getServerViaRemarks(remark) }
|
||||||
.filter { it.server.isNotNullEmpty() }
|
.filter { it.server.isNotNullEmpty() }
|
||||||
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
||||||
.filter { it.configType != EConfigType.CUSTOM }
|
.filter { !it.configType.isComplexType() }
|
||||||
.filter { it.configType != EConfigType.POLICYGROUP }
|
|
||||||
.filter { it.configType != EConfigType.PROXYCHAIN }
|
|
||||||
.toList()
|
.toList()
|
||||||
.reversed()
|
.reversed()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ object CoreOutboundBuilder {
|
|||||||
|
|
||||||
NetworkType.WS.type -> {
|
NetworkType.WS.type -> {
|
||||||
val wssetting = OutboundBean.StreamSettingsBean.WsSettingsBean()
|
val wssetting = OutboundBean.StreamSettingsBean.WsSettingsBean()
|
||||||
wssetting.headers.Host = host.orEmpty()
|
wssetting.host = host.orEmpty()
|
||||||
sni = host
|
sni = host
|
||||||
wssetting.path = path ?: "/"
|
wssetting.path = path ?: "/"
|
||||||
streamSettings.wsSettings = wssetting
|
streamSettings.wsSettings = wssetting
|
||||||
@@ -685,4 +685,4 @@ object CoreOutboundBuilder {
|
|||||||
}
|
}
|
||||||
return resolvedIps.first()
|
return resolvedIps.first()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.v2ray.ang.contracts.ServiceControl
|
|||||||
import com.v2ray.ang.dto.OutboundTrafficStat
|
import com.v2ray.ang.dto.OutboundTrafficStat
|
||||||
import com.v2ray.ang.dto.entities.ProfileItem
|
import com.v2ray.ang.dto.entities.ProfileItem
|
||||||
import com.v2ray.ang.enums.EConfigType
|
import com.v2ray.ang.enums.EConfigType
|
||||||
|
import com.v2ray.ang.extension.isComplexType
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.NotificationManager
|
import com.v2ray.ang.handler.NotificationManager
|
||||||
@@ -145,9 +146,7 @@ object CoreServiceManager {
|
|||||||
error(context.getString(R.string.toast_config_file_invalid))
|
error(context.getString(R.string.toast_config_file_invalid))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.configType != EConfigType.CUSTOM
|
if (!config.configType.isComplexType()
|
||||||
&& config.configType != EConfigType.POLICYGROUP
|
|
||||||
&& config.configType != EConfigType.PROXYCHAIN
|
|
||||||
&& !Utils.isValidUrl(config.server)
|
&& !Utils.isValidUrl(config.server)
|
||||||
&& !Utils.isPureIpAddress(config.server.orEmpty())
|
&& !Utils.isPureIpAddress(config.server.orEmpty())
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -197,13 +197,12 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
data class WsSettingsBean(
|
data class WsSettingsBean(
|
||||||
var path: String? = null,
|
var path: String? = null,
|
||||||
var headers: HeadersBean = HeadersBean(),
|
var host: String? = null,
|
||||||
|
var headers: Map<String, String>? = null,
|
||||||
val maxEarlyData: Int? = null,
|
val maxEarlyData: Int? = null,
|
||||||
val useBrowserForwarding: Boolean? = null,
|
val useBrowserForwarding: Boolean? = null,
|
||||||
val acceptProxyProtocol: Boolean? = null
|
val acceptProxyProtocol: Boolean? = null
|
||||||
) {
|
)
|
||||||
data class HeadersBean(var Host: String = "")
|
|
||||||
}
|
|
||||||
|
|
||||||
data class HttpupgradeSettingsBean(
|
data class HttpupgradeSettingsBean(
|
||||||
var path: String? = null,
|
var path: String? = null,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.v2ray.ang.AngApplication
|
import com.v2ray.ang.AngApplication
|
||||||
|
import com.v2ray.ang.enums.EConfigType
|
||||||
import es.dmoral.toasty.Toasty
|
import es.dmoral.toasty.Toasty
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@@ -200,4 +201,22 @@ fun String.matchesPattern(regex: Regex?, keyword: String?, ignoreCase: Boolean =
|
|||||||
}
|
}
|
||||||
return regex?.containsMatchIn(this)
|
return regex?.containsMatchIn(this)
|
||||||
?: this.contains(keyword, ignoreCase = ignoreCase)
|
?: this.contains(keyword, ignoreCase = ignoreCase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the config type is a group type (PolicyGroup or ProxyChain).
|
||||||
|
*
|
||||||
|
* @return True if the config type is PolicyGroup or ProxyChain, false otherwise.
|
||||||
|
*/
|
||||||
|
fun EConfigType.isGroupType(): Boolean {
|
||||||
|
return this == EConfigType.POLICYGROUP || this == EConfigType.PROXYCHAIN
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the config type is a complex type (Custom, PolicyGroup, or ProxyChain).
|
||||||
|
*
|
||||||
|
* @return True if the config type is Custom, PolicyGroup, or ProxyChain, false otherwise.
|
||||||
|
*/
|
||||||
|
fun EConfigType.isComplexType(): Boolean {
|
||||||
|
return this == EConfigType.CUSTOM || this == EConfigType.POLICYGROUP || this == EConfigType.PROXYCHAIN
|
||||||
|
}
|
||||||
|
|||||||
@@ -234,15 +234,8 @@ object AngConfigManager {
|
|||||||
if (servers == null) {
|
if (servers == null) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
// Find the currently selected server that matches the subscription ID
|
// Find the currently selected server that belongs to the same subscription before replacement.
|
||||||
val removedSelected = if (subid.isNotBlank() && !append) {
|
val removedSelected = getRemovedSelectedProfile(subid, append)
|
||||||
MmkvManager.getSelectServer()
|
|
||||||
.takeIf { it?.isNotBlank() == true }
|
|
||||||
?.let { MmkvManager.decodeServerConfig(it) }
|
|
||||||
?.takeIf { it.subscriptionId == subid }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
val subItem = MmkvManager.decodeSubscription(subid)
|
val subItem = MmkvManager.decodeSubscription(subid)
|
||||||
|
|
||||||
@@ -288,7 +281,6 @@ object AngConfigManager {
|
|||||||
|
|
||||||
// Read serverList once
|
// Read serverList once
|
||||||
val serverList = MmkvManager.decodeServerList(subid)
|
val serverList = MmkvManager.decodeServerList(subid)
|
||||||
var needSetSelected = MmkvManager.getSelectServer().isNullOrBlank()
|
|
||||||
|
|
||||||
configs.forEach { config ->
|
configs.forEach { config ->
|
||||||
val key = Utils.getUuid()
|
val key = Utils.getUuid()
|
||||||
@@ -297,10 +289,6 @@ object AngConfigManager {
|
|||||||
|
|
||||||
if (!serverList.contains(key)) {
|
if (!serverList.contains(key)) {
|
||||||
serverList.add(0, key)
|
serverList.add(0, key)
|
||||||
if (needSetSelected) {
|
|
||||||
MmkvManager.setSelectServer(key)
|
|
||||||
needSetSelected = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
keyToProfile[key] = config
|
keyToProfile[key] = config
|
||||||
}
|
}
|
||||||
@@ -323,7 +311,8 @@ object AngConfigManager {
|
|||||||
* @return Matched key or null
|
* @return Matched key or null
|
||||||
*/
|
*/
|
||||||
private fun findMatchedProfileKey(keyToProfile: Map<String, ProfileItem>, target: ProfileItem?): String? {
|
private fun findMatchedProfileKey(keyToProfile: Map<String, ProfileItem>, target: ProfileItem?): String? {
|
||||||
if (keyToProfile.isEmpty() || target == null) return null
|
if (keyToProfile.isEmpty()) return null
|
||||||
|
if (target == null) return null
|
||||||
|
|
||||||
// Level 0: Full match (remarks + server + port + password)
|
// Level 0: Full match (remarks + server + port + password)
|
||||||
if (target.remarks.isNotBlank()) {
|
if (target.remarks.isNotBlank()) {
|
||||||
@@ -360,7 +349,20 @@ object AngConfigManager {
|
|||||||
isSameText(saved.server, target.server)
|
isSameText(saved.server, target.server)
|
||||||
}?.key?.let { return it }
|
}?.key?.let { return it }
|
||||||
|
|
||||||
return null
|
// If old selected node cannot be matched, fall back to the first imported config.
|
||||||
|
return keyToProfile.keys.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently selected profile if it belongs to the target subscription and will be replaced.
|
||||||
|
*/
|
||||||
|
private fun getRemovedSelectedProfile(subid: String, append: Boolean): ProfileItem? {
|
||||||
|
if (subid.isBlank() || append) return null
|
||||||
|
|
||||||
|
return MmkvManager.getSelectServer()
|
||||||
|
.takeIf { it?.isNotBlank() == true }
|
||||||
|
?.let { MmkvManager.decodeServerConfig(it) }
|
||||||
|
?.takeIf { it.subscriptionId == subid }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -396,18 +398,25 @@ object AngConfigManager {
|
|||||||
JsonUtil.fromJson(server, Array<Any>::class.java) ?: arrayOf()
|
JsonUtil.fromJson(server, Array<Any>::class.java) ?: arrayOf()
|
||||||
|
|
||||||
if (serverList.isNotEmpty()) {
|
if (serverList.isNotEmpty()) {
|
||||||
|
val removedSelected = getRemovedSelectedProfile(subid, append)
|
||||||
if (!append) {
|
if (!append) {
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
}
|
}
|
||||||
var count = 0
|
var count = 0
|
||||||
|
val keyToProfile = mutableMapOf<String, ProfileItem>()
|
||||||
for (srv in serverList.reversed()) {
|
for (srv in serverList.reversed()) {
|
||||||
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
||||||
config.subscriptionId = subid
|
config.subscriptionId = subid
|
||||||
config.description = generateDescription(config)
|
config.description = generateDescription(config)
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
||||||
|
keyToProfile[key] = config
|
||||||
count += 1
|
count += 1
|
||||||
}
|
}
|
||||||
|
if (count > 0) {
|
||||||
|
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
|
||||||
|
matchKey?.let { MmkvManager.setSelectServer(it) }
|
||||||
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import com.v2ray.ang.databinding.FragmentGroupServerBinding
|
|||||||
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
||||||
import com.v2ray.ang.dto.entities.ProfileItem
|
import com.v2ray.ang.dto.entities.ProfileItem
|
||||||
import com.v2ray.ang.enums.EConfigType
|
import com.v2ray.ang.enums.EConfigType
|
||||||
|
import com.v2ray.ang.extension.isComplexType
|
||||||
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
|
||||||
@@ -269,9 +270,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean) {
|
override fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean) {
|
||||||
val isCustom = profile.configType == EConfigType.CUSTOM
|
val isCustom = profile.configType.isComplexType()
|
||||||
|| profile.configType == EConfigType.POLICYGROUP
|
|
||||||
|| profile.configType == EConfigType.PROXYCHAIN
|
|
||||||
|
|
||||||
val (shareOptions, skip) = if (more) {
|
val (shareOptions, skip) = if (more) {
|
||||||
val options = if (isCustom) share_method_more.asList().takeLast(3) else share_method_more.asList()
|
val options = if (isCustom) share_method_more.asList().takeLast(3) else share_method_more.asList()
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
@@ -18,6 +20,11 @@ import com.v2ray.ang.viewmodel.LogcatViewModel
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||||
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
||||||
@@ -45,6 +52,55 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun shareLogcat() {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val logText = viewModel.getAll().joinToString("\n")
|
||||||
|
|
||||||
|
val result = try {
|
||||||
|
val shareDir = File(cacheDir, "shared_logs").apply {
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
shareDir.listFiles()?.forEach { it.delete() }
|
||||||
|
|
||||||
|
val timestamp = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US).format(Date())
|
||||||
|
val logFile = File(shareDir, "v2rayNG_logcat_$timestamp.txt")
|
||||||
|
logFile.writeText(logText, Charsets.UTF_8)
|
||||||
|
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
this@LogcatActivity,
|
||||||
|
"${packageName}.cache",
|
||||||
|
logFile
|
||||||
|
)
|
||||||
|
|
||||||
|
uri to logFile.name
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
toast(e.localizedMessage ?: e.toString())
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(Intent.EXTRA_STREAM, result.first)
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, result.second)
|
||||||
|
putExtra(Intent.EXTRA_TITLE, result.second)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
clipData = ClipData.newUri(contentResolver, result.second, result.first)
|
||||||
|
}
|
||||||
|
|
||||||
|
startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
shareIntent,
|
||||||
|
getString(R.string.logcat_share)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_logcat, menu)
|
menuInflater.inflate(R.menu.menu_logcat, menu)
|
||||||
|
|
||||||
@@ -78,6 +134,11 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.share_all -> {
|
||||||
|
shareLogcat()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
R.id.clear_all -> {
|
R.id.clear_all -> {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
viewModel.clearLogcat()
|
viewModel.clearLogcat()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
@@ -14,6 +15,7 @@ import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
|
|||||||
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
|
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
|
||||||
import com.v2ray.ang.dto.entities.ProfileItem
|
import com.v2ray.ang.dto.entities.ProfileItem
|
||||||
import com.v2ray.ang.dto.entities.ServersCache
|
import com.v2ray.ang.dto.entities.ServersCache
|
||||||
|
import com.v2ray.ang.extension.isComplexType
|
||||||
import com.v2ray.ang.extension.nullIfBlank
|
import com.v2ray.ang.extension.nullIfBlank
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
@@ -58,7 +60,7 @@ class MainRecyclerAdapter(
|
|||||||
//Name address
|
//Name address
|
||||||
holder.itemMainBinding.tvName.text = profile.remarks
|
holder.itemMainBinding.tvName.text = profile.remarks
|
||||||
holder.itemMainBinding.tvStatistics.text = getAddress(profile)
|
holder.itemMainBinding.tvStatistics.text = getAddress(profile)
|
||||||
holder.itemMainBinding.tvType.text = profile.configType.name
|
holder.itemMainBinding.tvType.text = getProtocolDescription(profile)
|
||||||
|
|
||||||
//TestResult
|
//TestResult
|
||||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||||
@@ -140,6 +142,31 @@ class MainRecyclerAdapter(
|
|||||||
return subRemarks?.toString() ?: ""
|
return subRemarks?.toString() ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getProtocolDescription(profile: ProfileItem): String {
|
||||||
|
if (profile.configType.isComplexType()) {
|
||||||
|
return profile.configType.name
|
||||||
|
}
|
||||||
|
|
||||||
|
val parts = mutableListOf<String>()
|
||||||
|
parts.add(profile.configType.name)
|
||||||
|
|
||||||
|
// Transport: hide tcp or blank
|
||||||
|
profile.network?.let { net ->
|
||||||
|
if (net.isNotBlank() && !net.equals("tcp", ignoreCase = true)) {
|
||||||
|
parts.add(net)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security: hide blank or tls
|
||||||
|
profile.security?.let { sec ->
|
||||||
|
if (sec.isNotBlank() && !sec.equals("tls", ignoreCase = true)) {
|
||||||
|
parts.add(sec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.joinToString(" / ")
|
||||||
|
}
|
||||||
|
|
||||||
fun removeServerSub(guid: String, position: Int) {
|
fun removeServerSub(guid: String, position: Int) {
|
||||||
val idx = data.indexOfFirst { it.guid == guid }
|
val idx = data.indexOfFirst { it.guid == guid }
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
@@ -203,4 +230,4 @@ class MainRecyclerAdapter(
|
|||||||
|
|
||||||
override fun onItemDismiss(position: Int) {
|
override fun onItemDismiss(position: Int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.v2ray.ang.contracts.BaseAdapterListener
|
|||||||
import com.v2ray.ang.databinding.ActivityServerProxyChainBinding
|
import com.v2ray.ang.databinding.ActivityServerProxyChainBinding
|
||||||
import com.v2ray.ang.dto.entities.ProfileItem
|
import com.v2ray.ang.dto.entities.ProfileItem
|
||||||
import com.v2ray.ang.enums.EConfigType
|
import com.v2ray.ang.enums.EConfigType
|
||||||
|
import com.v2ray.ang.extension.isComplexType
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
@@ -109,10 +110,7 @@ class ServerProxyChainActivity : BaseActivity() {
|
|||||||
|
|
||||||
val invalidMembers = chainMembers.filter { member ->
|
val invalidMembers = chainMembers.filter { member ->
|
||||||
val profile = SettingsManager.getServerViaRemarks(member)
|
val profile = SettingsManager.getServerViaRemarks(member)
|
||||||
profile == null
|
profile == null || profile.configType.isComplexType()
|
||||||
|| profile.configType == EConfigType.CUSTOM
|
|
||||||
|| profile.configType == EConfigType.POLICYGROUP
|
|
||||||
|| profile.configType == EConfigType.PROXYCHAIN
|
|
||||||
}
|
}
|
||||||
if (invalidMembers.isNotEmpty()) {
|
if (invalidMembers.isNotEmpty()) {
|
||||||
toast(getString(R.string.server_proxy_chain_members_invalid, invalidMembers.joinToString(", ")))
|
toast(getString(R.string.server_proxy_chain_members_invalid, invalidMembers.joinToString(", ")))
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import com.v2ray.ang.dto.SubscriptionUpdateResult
|
|||||||
import com.v2ray.ang.dto.TestServiceMessage
|
import com.v2ray.ang.dto.TestServiceMessage
|
||||||
import com.v2ray.ang.dto.entities.ServersCache
|
import com.v2ray.ang.dto.entities.ServersCache
|
||||||
import com.v2ray.ang.dto.entities.SubscriptionCache
|
import com.v2ray.ang.dto.entities.SubscriptionCache
|
||||||
|
import com.v2ray.ang.extension.isComplexType
|
||||||
import com.v2ray.ang.extension.matchesPattern
|
import com.v2ray.ang.extension.matchesPattern
|
||||||
import com.v2ray.ang.extension.toastError
|
import com.v2ray.ang.extension.toastError
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
@@ -296,16 +297,28 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes duplicate servers.
|
* Removes duplicate servers.
|
||||||
|
* Excludes servers with complex types (Custom, PolicyGroup, or ProxyChain) from duplicate comparison.
|
||||||
* @return The number of removed servers.
|
* @return The number of removed servers.
|
||||||
*/
|
*/
|
||||||
fun removeDuplicateServer(): Int {
|
fun removeDuplicateServer(): Int {
|
||||||
val serversCacheCopy = serversCache.toList().toMutableList()
|
val serversCacheCopy = serversCache.toList().toMutableList()
|
||||||
val deleteServer = mutableListOf<String>()
|
val deleteServer = mutableListOf<String>()
|
||||||
|
|
||||||
serversCacheCopy.forEachIndexed { index, sc ->
|
serversCacheCopy.forEachIndexed { index, sc ->
|
||||||
val profile = sc.profile
|
val profile = sc.profile
|
||||||
|
// Skip if this profile has a complex config type
|
||||||
|
if (profile.configType.isComplexType()) {
|
||||||
|
return@forEachIndexed
|
||||||
|
}
|
||||||
|
|
||||||
serversCacheCopy.forEachIndexed { index2, sc2 ->
|
serversCacheCopy.forEachIndexed { index2, sc2 ->
|
||||||
if (index2 > index) {
|
if (index2 > index) {
|
||||||
val profile2 = sc2.profile
|
val profile2 = sc2.profile
|
||||||
|
// Skip if the second profile has a complex config type
|
||||||
|
if (profile2.configType.isComplexType()) {
|
||||||
|
return@forEachIndexed
|
||||||
|
}
|
||||||
|
|
||||||
if (profile == profile2 && !deleteServer.contains(sc2.guid)) {
|
if (profile == profile2 && !deleteServer.contains(sc2.guid)) {
|
||||||
deleteServer.add(sc2.guid)
|
deleteServer.add(sc2.guid)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,4 +17,9 @@
|
|||||||
android:icon="@drawable/ic_copy"
|
android:icon="@drawable/ic_copy"
|
||||||
android:title="@string/logcat_copy"
|
android:title="@string/logcat_copy"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/share_all"
|
||||||
|
android:icon="@drawable/ic_share_24dp"
|
||||||
|
android:title="@string/logcat_share"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
</menu>
|
</menu>
|
||||||
@@ -286,6 +286,7 @@
|
|||||||
<string name="title_logcat">Logcat</string>
|
<string name="title_logcat">Logcat</string>
|
||||||
<string name="logcat_copy">نسخ</string>
|
<string name="logcat_copy">نسخ</string>
|
||||||
<string name="logcat_clear">مسح</string>
|
<string name="logcat_clear">مسح</string>
|
||||||
|
<string name="logcat_share">مشاركة السجل</string>
|
||||||
<string name="title_service_restart">إعادة تشغيل الخدمة</string>
|
<string name="title_service_restart">إعادة تشغيل الخدمة</string>
|
||||||
<string name="title_del_all_config">حذف جميع الإعدادات (قبل أول خطوة)</string>
|
<string name="title_del_all_config">حذف جميع الإعدادات (قبل أول خطوة)</string>
|
||||||
<string name="title_del_duplicate_config">حذف الإعدادات المكررة (2)</string>
|
<string name="title_del_duplicate_config">حذف الإعدادات المكررة (2)</string>
|
||||||
|
|||||||
@@ -286,6 +286,7 @@
|
|||||||
<string name="title_logcat">লগক্যাট</string>
|
<string name="title_logcat">লগক্যাট</string>
|
||||||
<string name="logcat_copy">কপি করুন</string>
|
<string name="logcat_copy">কপি করুন</string>
|
||||||
<string name="logcat_clear">স্পষ্ট করুন</string>
|
<string name="logcat_clear">স্পষ্ট করুন</string>
|
||||||
|
<string name="logcat_share">লগ শেয়ার করুন</string>
|
||||||
<string name="title_service_restart">সার্ভিস পুনরায় চালু করুন</string>
|
<string name="title_service_restart">সার্ভিস পুনরায় চালু করুন</string>
|
||||||
<string name="title_del_all_config">সব কনফিগারেশন মুছে ফেলুন</string>
|
<string name="title_del_all_config">সব কনফিগারেশন মুছে ফেলুন</string>
|
||||||
<string name="title_del_duplicate_config">ডুপ্লিকেট কনফিগারেশন মুছে ফেলুন</string>
|
<string name="title_del_duplicate_config">ডুপ্লিকেট কনফিগারেশন মুছে ফেলুন</string>
|
||||||
|
|||||||
@@ -286,6 +286,7 @@
|
|||||||
<string name="title_logcat">گوزارشا</string>
|
<string name="title_logcat">گوزارشا</string>
|
||||||
<string name="logcat_copy">لف گیری</string>
|
<string name="logcat_copy">لف گیری</string>
|
||||||
<string name="logcat_clear">روفتن</string>
|
<string name="logcat_clear">روفتن</string>
|
||||||
|
<string name="logcat_share">Share log</string>
|
||||||
<string name="title_service_restart">ره وندن دووارته خدمات</string>
|
<string name="title_service_restart">ره وندن دووارته خدمات</string>
|
||||||
<string name="title_del_all_config">پاک کردن پوی کانفیگا بونکۊ سکویی</string>
|
<string name="title_del_all_config">پاک کردن پوی کانفیگا بونکۊ سکویی</string>
|
||||||
<string name="title_del_duplicate_config">پاک کردن کانفیگا تکراری بونکۊ سکویی</string>
|
<string name="title_del_duplicate_config">پاک کردن کانفیگا تکراری بونکۊ سکویی</string>
|
||||||
|
|||||||
@@ -283,6 +283,7 @@
|
|||||||
<string name="title_logcat">گزارشات</string>
|
<string name="title_logcat">گزارشات</string>
|
||||||
<string name="logcat_copy">کپی</string>
|
<string name="logcat_copy">کپی</string>
|
||||||
<string name="logcat_clear">پاک کردن</string>
|
<string name="logcat_clear">پاک کردن</string>
|
||||||
|
<string name="logcat_share">اشتراکگذاری گزارش</string>
|
||||||
<string name="title_service_restart">راهاندازی مجدد خدمات</string>
|
<string name="title_service_restart">راهاندازی مجدد خدمات</string>
|
||||||
<string name="title_del_all_config">حذف تمام کانفیگ های گروه فعلی</string>
|
<string name="title_del_all_config">حذف تمام کانفیگ های گروه فعلی</string>
|
||||||
<string name="title_del_duplicate_config">حذف کانفیگ های تکراری گروه فعلی</string>
|
<string name="title_del_duplicate_config">حذف کانفیگ های تکراری گروه فعلی</string>
|
||||||
|
|||||||
@@ -291,6 +291,7 @@
|
|||||||
<string name="title_logcat">Журнал</string>
|
<string name="title_logcat">Журнал</string>
|
||||||
<string name="logcat_copy">Копировать</string>
|
<string name="logcat_copy">Копировать</string>
|
||||||
<string name="logcat_clear">Очистить</string>
|
<string name="logcat_clear">Очистить</string>
|
||||||
|
<string name="logcat_share">Поделиться логом</string>
|
||||||
<string name="title_service_restart">Перезапуск службы</string>
|
<string name="title_service_restart">Перезапуск службы</string>
|
||||||
<string name="title_del_all_config">Удалить профили</string>
|
<string name="title_del_all_config">Удалить профили</string>
|
||||||
<string name="title_del_duplicate_config">Удалить дубликаты профилей</string>
|
<string name="title_del_duplicate_config">Удалить дубликаты профилей</string>
|
||||||
|
|||||||
@@ -286,6 +286,7 @@
|
|||||||
<string name="title_logcat">Logcat</string>
|
<string name="title_logcat">Logcat</string>
|
||||||
<string name="logcat_copy">Sao chép</string>
|
<string name="logcat_copy">Sao chép</string>
|
||||||
<string name="logcat_clear">Xoá</string>
|
<string name="logcat_clear">Xoá</string>
|
||||||
|
<string name="logcat_share">Chia sẻ nhật ký</string>
|
||||||
<string name="title_service_restart">Khởi động lại dịch vụ lõi</string>
|
<string name="title_service_restart">Khởi động lại dịch vụ lõi</string>
|
||||||
<string name="title_del_all_config">Xoá tất cả cấu hình</string>
|
<string name="title_del_all_config">Xoá tất cả cấu hình</string>
|
||||||
<string name="title_del_duplicate_config">Xoá cấu hình trùng lặp</string>
|
<string name="title_del_duplicate_config">Xoá cấu hình trùng lặp</string>
|
||||||
|
|||||||
@@ -286,6 +286,7 @@
|
|||||||
<string name="title_logcat">Logcat</string>
|
<string name="title_logcat">Logcat</string>
|
||||||
<string name="logcat_copy">复制</string>
|
<string name="logcat_copy">复制</string>
|
||||||
<string name="logcat_clear">清除</string>
|
<string name="logcat_clear">清除</string>
|
||||||
|
<string name="logcat_share">分享日志</string>
|
||||||
<string name="title_service_restart">服务重启</string>
|
<string name="title_service_restart">服务重启</string>
|
||||||
<string name="title_del_all_config">删除配置</string>
|
<string name="title_del_all_config">删除配置</string>
|
||||||
<string name="title_del_duplicate_config">删除重复配置</string>
|
<string name="title_del_duplicate_config">删除重复配置</string>
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
<string name="title_logcat">Logcat</string>
|
<string name="title_logcat">Logcat</string>
|
||||||
<string name="logcat_copy">複製</string>
|
<string name="logcat_copy">複製</string>
|
||||||
<string name="logcat_clear">清除</string>
|
<string name="logcat_clear">清除</string>
|
||||||
|
<string name="logcat_share">分享日誌</string>
|
||||||
<string name="title_service_restart">重啟服務</string>
|
<string name="title_service_restart">重啟服務</string>
|
||||||
<string name="title_del_all_config">刪除設定</string>
|
<string name="title_del_all_config">刪除設定</string>
|
||||||
<string name="title_del_duplicate_config">刪除重複設定</string>
|
<string name="title_del_duplicate_config">刪除重複設定</string>
|
||||||
|
|||||||
@@ -293,6 +293,7 @@
|
|||||||
<string name="title_logcat">Logcat</string>
|
<string name="title_logcat">Logcat</string>
|
||||||
<string name="logcat_copy">Copy</string>
|
<string name="logcat_copy">Copy</string>
|
||||||
<string name="logcat_clear">Clear</string>
|
<string name="logcat_clear">Clear</string>
|
||||||
|
<string name="logcat_share">Share log</string>
|
||||||
<string name="title_service_restart">Service restart</string>
|
<string name="title_service_restart">Service restart</string>
|
||||||
<string name="title_del_all_config">Delete config</string>
|
<string name="title_del_all_config">Delete config</string>
|
||||||
<string name="title_del_duplicate_config">Delete duplicate config</string>
|
<string name="title_del_duplicate_config">Delete duplicate config</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user