Skip to content

Commit ee41c7e

Browse files
fix: protect pinned servers from all delete flows and subscription updates
1 parent d537d23 commit ee41c7e

20 files changed

Lines changed: 124 additions & 25 deletions

V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,8 +408,10 @@ object AngConfigManager {
408408
}
409409

410410
fun removeInvalidServer(subId: String) {
411+
val pinnedServers = MmkvManager.decodePinnedServers()
411412
val serverList = MmkvManager.decodeServerList(subId)
412413
val invalidServers = serverList.filter {
414+
if (pinnedServers.contains(it)) return@filter false
413415
val aff = MmkvManager.decodeServerAffiliationInfo(it)
414416
aff != null && aff.testDelayMillis < 0L
415417
}

V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,16 @@ object MmkvManager {
278278
}
279279
}
280280

281+
// Pinned servers among the ones being replaced must survive the update, the
282+
// same way the selected server does: neither their payload nor their spot in
283+
// the group index should be dropped just because a subscription refresh happened.
284+
val pinnedServers = decodePinnedServers()
285+
val pinnedReplacedServers = if (append) {
286+
emptyList()
287+
} else {
288+
replacedServers.filter { pinnedServers.contains(it) }
289+
}
290+
281291
// Publish the new group index.
282292
val serverList = if (append) {
283293
decodeServerList(subscriptionId)
@@ -290,6 +300,13 @@ object MmkvManager {
290300
serverList.add(0, guid)
291301
}
292302
}
303+
// Re-append pinned servers that were dropped by the replacement so they keep
304+
// showing up in this group after the subscription update.
305+
pinnedReplacedServers.forEach { guid ->
306+
if (indexedServers.add(guid)) {
307+
serverList.add(guid)
308+
}
309+
}
293310
encodeServerList(serverList, subscriptionId)
294311

295312
replacementSelection?.let { guid ->
@@ -302,11 +319,11 @@ object MmkvManager {
302319
if (replacedServers.isEmpty()) return@withProfileIndexLock
303320

304321
// Only now, after the replacement batch is safely published, drop the old payloads.
305-
val protectedServer = replacementSelection ?: previousSelection
322+
val protectedServers = setOfNotNull(replacementSelection ?: previousSelection) + pinnedReplacedServers
306323
val removablePayloads = ProfileReplacement.findRemovablePayloads(
307324
replacedServers = replacedServers,
308325
replacementServers = profiles.keys,
309-
protectedServer = protectedServer,
326+
protectedServers = protectedServers,
310327
)
311328
removeProfilePayloads(removablePayloads)
312329
}
@@ -615,29 +632,57 @@ object MmkvManager {
615632
}
616633
}
617634

635+
/**
636+
* Removes every server, except pinned ones, which are always kept regardless of
637+
* which group/subscription they belong to.
638+
*/
618639
fun removeAllServer(): Int {
619-
val count = profileFullStorage.allKeys()?.count() ?: 0
620-
profileFullStorage.clearAll()
621-
serverAffStorage.clearAll()
622-
serverRawStorage.clearAll()
640+
val pinnedServers = decodePinnedServers()
641+
val subsList = decodeSubsList().toMutableList()
642+
if (!subsList.contains(DEFAULT_SUBSCRIPTION_ID)) {
643+
subsList.add(DEFAULT_SUBSCRIPTION_ID)
644+
}
623645

624-
decodeSubscriptions().forEach { sub ->
625-
encodeServerList(mutableListOf(), sub.guid)
646+
var removedCount = 0
647+
subsList.forEach { subId ->
648+
val serverList = decodeServerList(subId)
649+
val (kept, removed) = serverList.partition { pinnedServers.contains(it) }
650+
if (removed.isNotEmpty()) {
651+
removed.forEach { guid ->
652+
if (getSelectServer() == guid) {
653+
mainStorage.remove(KEY_SELECTED_SERVER)
654+
}
655+
}
656+
removeProfilePayloads(removed)
657+
removedCount += removed.size
658+
encodeServerList(kept.toMutableList(), subId)
659+
}
626660
}
627-
return count
661+
return removedCount
628662
}
629663

664+
/**
665+
* Removes servers with a failed (negative) test delay. Pinned servers are always
666+
* kept, even if their last test result was invalid.
667+
*/
630668
fun removeInvalidServer(guid: String): Int {
631669
var count = 0
632670
if (guid.isNotEmpty()) {
671+
if (isServerPinned(guid)) {
672+
return 0
673+
}
633674
decodeServerAffiliationInfo(guid)?.let { aff ->
634675
if (aff.testDelayMillis < 0L) {
635676
removeServer(guid)
636677
count++
637678
}
638679
}
639680
} else {
681+
val pinnedServers = decodePinnedServers()
640682
serverAffStorage.allKeys()?.forEach { key ->
683+
if (pinnedServers.contains(key)) {
684+
return@forEach
685+
}
641686
decodeServerAffiliationInfo(key)?.let { aff ->
642687
if (aff.testDelayMillis < 0L) {
643688
removeServer(key)

V2rayNG/app/src/main/java/com/v2ray/ang/handler/ProfileReplacement.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,17 @@ internal object ProfileReplacement {
6161
*
6262
* @param replacedServers GUIDs that were indexed under the group before this replacement.
6363
* @param replacementServers GUIDs of the newly published batch.
64-
* @param protectedServer The currently selected GUID (new or old), never removed.
64+
* @param protectedServers GUIDs that are never removed: the currently selected server
65+
* (new or old) plus any pinned server. Pinned servers survive subscription updates the
66+
* same way the selected server does.
6567
*/
6668
fun findRemovablePayloads(
6769
replacedServers: Collection<String>,
6870
replacementServers: Set<String>,
69-
protectedServer: String?,
71+
protectedServers: Set<String>,
7072
): Set<String> {
7173
return replacedServers.filterTo(linkedSetOf()) { guid ->
72-
guid != protectedServer && guid !in replacementServers
74+
guid !in protectedServers && guid !in replacementServers
7375
}
7476
}
7577

V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/GroupServerFragment.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,11 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
290290
return
291291
}
292292

293+
if (MmkvManager.isServerPinned(guid)) {
294+
ownerActivity.snackbarDefault(getString(R.string.toast_pinned_server_delete_blocked), title = getString(R.string.title_alerter_info))
295+
return
296+
}
297+
293298
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) {
294299
showDeleteConfirmDialog(context = ownerActivity, messageRes = R.string.del_config_dialog_comfirm_message) {
295300
removeServerSub(guid, position)

V2rayNG/app/src/main/java/com/v2ray/ang/ui/main/MainViewModel.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
332332
fun removeDuplicateServer(): Int {
333333
val serversCacheCopy = serversCache.toList().toMutableList()
334334
val deleteServer = mutableListOf<String>()
335+
val pinnedServers = MmkvManager.decodePinnedServers()
335336

336337
serversCacheCopy.forEachIndexed { index, sc ->
337338
val profile = sc.profile
@@ -346,7 +347,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
346347
return@forEachIndexed
347348
}
348349

349-
if (profile == profile2 && !deleteServer.contains(sc2.guid)) {
350+
// Pinned servers are never treated as the removable duplicate, so
351+
// pinning a server is enough to keep it even if an identical
352+
// config exists elsewhere in the list.
353+
if (profile == profile2 && !deleteServer.contains(sc2.guid) && !pinnedServers.contains(sc2.guid)) {
350354
deleteServer.add(sc2.guid)
351355
}
352356
}
@@ -364,11 +368,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
364368
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
365369
MmkvManager.removeAllServer()
366370
} else {
367-
val serversCopy = serversCache.toList()
371+
val pinnedServers = MmkvManager.decodePinnedServers()
372+
val serversCopy = serversCache.toList().filterNot { pinnedServers.contains(it.guid) }
368373
for (item in serversCopy) {
369374
MmkvManager.removeServer(item.guid)
370375
}
371-
serversCache.toList().count()
376+
serversCopy.count()
372377
}
373378
return count
374379
}
@@ -380,6 +385,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
380385
} else {
381386
val serversCopy = serversCache.toList()
382387
for (item in serversCopy) {
388+
// MmkvManager.removeInvalidServer already skips pinned servers.
383389
count += MmkvManager.removeInvalidServer(item.guid)
384390
}
385391
}

V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerCustomConfigActivity.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ class ServerCustomConfigActivity : BaseActivity() {
108108

109109
private fun deleteServer(): Boolean {
110110
if (editGuid.isNotEmpty()) {
111+
if (MmkvManager.isServerPinned(editGuid)) {
112+
snackbarError(getString(R.string.toast_pinned_server_delete_blocked), title = getString(R.string.title_alerter_error))
113+
return true
114+
}
111115
showDeleteConfirmDialog(context = this, messageRes = R.string.del_config_dialog_comfirm_message) {
112116
MmkvManager.removeServer(editGuid)
113117
finish()

V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerGroupActivity.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ class ServerGroupActivity : BaseActivity() {
174174

175175
private fun deleteServer(): Boolean {
176176
if (editGuid.isNotEmpty()) {
177+
if (MmkvManager.isServerPinned(editGuid)) {
178+
snackbarError(getString(R.string.toast_pinned_server_delete_blocked), title = getString(R.string.title_alerter_error))
179+
return true
180+
}
177181
showDeleteConfirmDialog(context = this, messageRes = R.string.del_config_dialog_comfirm_message) {
178182
MmkvManager.removeServer(editGuid)
179183
finish()

V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerHysteria2Activity.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ class ServerHysteria2Activity : BaseActivity() {
181181

182182
private fun deleteServer(): Boolean {
183183
if (editGuid.isNotEmpty()) {
184-
if (editGuid != MmkvManager.getSelectServer()) {
184+
if (editGuid != MmkvManager.getSelectServer() && !MmkvManager.isServerPinned(editGuid)) {
185185
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) {
186186
showDeleteConfirmDialog(context = this, messageRes = R.string.del_config_dialog_comfirm_message) {
187187
MmkvManager.removeServer(editGuid)
@@ -191,6 +191,8 @@ class ServerHysteria2Activity : BaseActivity() {
191191
MmkvManager.removeServer(editGuid)
192192
finish()
193193
}
194+
} else if (MmkvManager.isServerPinned(editGuid)) {
195+
snackbarError(getString(R.string.toast_pinned_server_delete_blocked), title = getString(R.string.title_alerter_error))
194196
} else {
195197
snackbarError(getString(R.string.toast_action_not_allowed), title = getString(R.string.title_alerter_error))
196198
}

V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerProxyChainActivity.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ class ServerProxyChainActivity : BaseActivity() {
149149

150150
private fun deleteServer(): Boolean {
151151
if (editGuid.isNotEmpty()) {
152-
if (editGuid != MmkvManager.getSelectServer()) {
152+
if (editGuid != MmkvManager.getSelectServer() && !MmkvManager.isServerPinned(editGuid)) {
153153
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) {
154154
showDeleteConfirmDialog(context = this, messageRes = R.string.del_config_dialog_comfirm_message) {
155155
MmkvManager.removeServer(editGuid)
@@ -159,6 +159,8 @@ class ServerProxyChainActivity : BaseActivity() {
159159
MmkvManager.removeServer(editGuid)
160160
finish()
161161
}
162+
} else if (MmkvManager.isServerPinned(editGuid)) {
163+
snackbarDefault(R.string.toast_pinned_server_delete_blocked, title = getString(R.string.title_alerter_info))
162164
} else {
163165
snackbarDefault(R.string.toast_action_not_allowed, title = getString(R.string.title_alerter_info))
164166
}

V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerShadowsocksActivity.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ class ServerShadowsocksActivity : BaseActivity() {
201201

202202
private fun deleteServer(): Boolean {
203203
if (editGuid.isNotEmpty()) {
204-
if (editGuid != MmkvManager.getSelectServer()) {
204+
if (editGuid != MmkvManager.getSelectServer() && !MmkvManager.isServerPinned(editGuid)) {
205205
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) {
206206
showDeleteConfirmDialog(context = this, messageRes = R.string.del_config_dialog_comfirm_message) {
207207
MmkvManager.removeServer(editGuid)
@@ -211,6 +211,8 @@ class ServerShadowsocksActivity : BaseActivity() {
211211
MmkvManager.removeServer(editGuid)
212212
finish()
213213
}
214+
} else if (MmkvManager.isServerPinned(editGuid)) {
215+
snackbarError(getString(R.string.toast_pinned_server_delete_blocked), title = getString(R.string.title_alerter_error))
214216
} else {
215217
snackbarError(getString(R.string.toast_action_not_allowed), title = getString(R.string.title_alerter_error))
216218
}

0 commit comments

Comments
 (0)