From 84ab54fea246353e683f3177db1c3e80d82c42da Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:58:01 +0800 Subject: [PATCH] Migrate server storage to subscription lists Introduce subscription-based server storage and migration from legacy KEY_ANG_CONFIGS. Added DEFAULT_SUBSCRIPTION_ID and new MmkvManager APIs to encode/decode server lists per subscription, decodeAllServerList, and readLegacyServerList; included helper getSubscriptionId and protection against deleting the default subscription. Refactored SettingsManager into initApp to ensure defaults, create a default subscription, initialize routing rulesets and perform server-list migration and other migrations. Updated callers (V2rayConfigManager, BackupActivity, GroupServerFragment, SubEditActivity, SubSettingRecyclerAdapter, TaskerActivity, MainViewModel) to use the new APIs and to handle subscriptionId where appropriate (including UI changes to hide delete for default subscription). Preserved legacy logic as commented references and added migration markers to avoid repeated migrations. Overall change enables grouping servers by subscription and provides a migration path for existing installations. --- .../main/java/com/v2ray/ang/AngApplication.kt | 8 +- .../src/main/java/com/v2ray/ang/AppConfig.kt | 3 + .../java/com/v2ray/ang/handler/MmkvManager.kt | 137 +++++++++++++++++- .../com/v2ray/ang/handler/SettingsManager.kt | 114 ++++++++++++--- .../v2ray/ang/handler/V2rayConfigManager.kt | 6 +- .../java/com/v2ray/ang/ui/BackupActivity.kt | 3 + .../com/v2ray/ang/ui/GroupServerFragment.kt | 1 + .../java/com/v2ray/ang/ui/SubEditActivity.kt | 2 +- .../v2ray/ang/ui/SubSettingRecyclerAdapter.kt | 3 + .../java/com/v2ray/ang/ui/TaskerActivity.kt | 2 +- .../com/v2ray/ang/viewmodel/MainViewModel.kt | 75 ++++++---- 11 files changed, 296 insertions(+), 58 deletions(-) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt index a5ea796c..55fc2e26 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt @@ -34,14 +34,12 @@ class AngApplication : MultiDexApplication() { MMKV.initialize(this) - // Ensure critical preference defaults are present in MMKV early - SettingsManager.ensureDefaultSettings() - SettingsManager.setNightMode() // Initialize WorkManager with the custom configuration WorkManager.initialize(this, workManagerConfiguration) - SettingsManager.initRoutingRulesets(this) - SettingsManager.migrateHysteria2PinSHA256() + // Ensure critical preference defaults are present in MMKV early + SettingsManager.initApp(this) + SettingsManager.setNightMode() es.dmoral.toasty.Toasty.Config.getInstance() .setGravity(android.view.Gravity.BOTTOM, 0, 300) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt index 598c3b85..5413bef0 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt @@ -16,6 +16,9 @@ object AppConfig { /** Legacy configuration keys. */ const val ANG_CONFIG = "ang_config" + // Default subscription ID for ungrouped servers + const val DEFAULT_SUBSCRIPTION_ID = "__default_subscription__" + /** Preferences mapped to MMKV storage. */ const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled" const val PREF_ROUTE_ONLY_ENABLED = "pref_route_only_enabled" diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt index ca3c4e1d..c05b0664 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt @@ -1,6 +1,7 @@ package com.v2ray.ang.handler import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig.DEFAULT_SUBSCRIPTION_ID import com.v2ray.ang.AppConfig.PREF_IS_BOOTED import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET import com.v2ray.ang.dto.AssetUrlCache @@ -28,6 +29,7 @@ object MmkvManager { private const val ID_SETTING = "SETTING" private const val KEY_SELECTED_SERVER = "SELECTED_SERVER" private const val KEY_ANG_CONFIGS = "ANG_CONFIGS" + private const val KEY_SUB_SERVER_PREFIX = "SUB_SERVERS_" private const val KEY_SUB_IDS = "SUB_IDS" private const val KEY_WEBDAV_CONFIG = "WEBDAV_CONFIG" @@ -44,6 +46,24 @@ object MmkvManager { //region Server + /** + * Reads the legacy server list from KEY_ANG_CONFIGS for migration. + * This method is for migration purposes only. + * + * @return The JSON string of legacy server list, or null if not exists. + */ + fun readLegacyServerList(): String? { + return mainStorage.decodeString(KEY_ANG_CONFIGS) + } + +// /** +// * Removes the legacy KEY_ANG_CONFIGS after migration. +// * This method is for migration purposes only. +// */ +// fun removeLegacyServerListKey() { +// mainStorage.remove(KEY_ANG_CONFIGS) +// } + /** * Gets the selected server GUID. * @@ -63,19 +83,65 @@ object MmkvManager { } /** - * Encodes the server list. + * Encodes the server list for a given subscription. + * Saves to the subscription's serverList (including default subscription for ungrouped servers). * * @param serverList The list of server GUIDs. + * @param subscriptionId The subscription ID. */ + fun encodeServerList(serverList: MutableList, subscriptionId: String) { + val subId = getSubscriptionId(subscriptionId) + val key = "$KEY_SUB_SERVER_PREFIX$subId" + mainStorage.encode(key, JsonUtil.toJson(serverList)) + } + + // Legacy method for compatibility + // TODO: Remove after migration and update all callers + /* fun encodeServerList(serverList: MutableList) { mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList)) } + */ /** - * Decodes the server list. + * Decodes the server list for a given subscription. + * If subscriptionId is empty, returns ungrouped servers. + * Otherwise, returns servers from the specified subscription's serverList. * + * @param subscriptionId The subscription ID. * @return The list of server GUIDs. */ + fun decodeServerList(subscriptionId: String): MutableList { + val subId = getSubscriptionId(subscriptionId) + val key = "$KEY_SUB_SERVER_PREFIX$subId" + val json = mainStorage.decodeString(key) + return if (json.isNullOrBlank()) { + mutableListOf() + } else { + JsonUtil.fromJson(json, Array::class.java)?.toMutableList() ?: mutableListOf() + } + } + + /** + * Decodes all server list (merged from all subscriptions including default subscription). + * Use this when you need the complete server list. + * + * @return The list of all server GUIDs. + */ + fun decodeAllServerList(): MutableList { + val allServers = mutableListOf() + + // Add servers from all subscriptions (including default subscription) + decodeSubsList().forEach { guid -> + allServers.addAll(decodeServerList(guid)) + } + + return allServers + } + + // Legacy method for compatibility - reads all servers + // TODO: Remove after migration and update all callers + /* fun decodeServerList(): MutableList { val json = mainStorage.decodeString(KEY_ANG_CONFIGS) return if (json.isNullOrBlank()) { @@ -84,6 +150,7 @@ object MmkvManager { JsonUtil.fromJson(json, Array::class.java)?.toMutableList() ?: mutableListOf() } } + */ /** * Decodes the server configuration. @@ -123,6 +190,21 @@ object MmkvManager { fun encodeServerConfig(guid: String, config: ProfileItem): String { val key = guid.ifBlank { Utils.getUuid() } profileFullStorage.encode(key, JsonUtil.toJson(config)) + + // Use default subscription for servers without subscription + val subId = getSubscriptionId(config.subscriptionId) + val serverList = decodeServerList(subId) + + if (!serverList.contains(key)) { + serverList.add(0, key) + encodeServerList(serverList, subId) + if (getSelectServer().isNullOrBlank()) { + mainStorage.encode(KEY_SELECTED_SERVER, key) + } + } + + // Legacy code - keep for reference during migration + /* val serverList = decodeServerList() if (!serverList.contains(key)) { serverList.add(0, key) @@ -131,6 +213,7 @@ object MmkvManager { mainStorage.encode(KEY_SELECTED_SERVER, key) } } + */ // val profile = ProfileLiteItem( // configType = config.configType, // subscriptionId = config.subscriptionId, @@ -151,12 +234,30 @@ object MmkvManager { if (guid.isBlank()) { return } + + // Get config to determine which subscription to update + val config = decodeServerConfig(guid) + val subId = getSubscriptionId(config?.subscriptionId) + + // Remove from appropriate server list + val serverList = decodeServerList(subId) + serverList.remove(guid) + encodeServerList(serverList, subId) + + // Legacy code - keep for reference during migration + /* if (getSelectServer() == guid) { mainStorage.remove(KEY_SELECTED_SERVER) } val serverList = decodeServerList() serverList.remove(guid) encodeServerList(serverList) + */ + + // Clean up storage + if (getSelectServer() == guid) { + mainStorage.remove(KEY_SELECTED_SERVER) + } profileFullStorage.remove(guid) //profileStorage.remove(guid) serverAffStorage.remove(guid) @@ -165,12 +266,26 @@ object MmkvManager { /** * Removes the server configurations via subscription ID. * - * @param subid The subscription ID. + * @param subscriptionId The subscription ID. */ - fun removeServerViaSubid(subid: String) { - if (subid.isBlank()) { - return + fun removeServerViaSubid(subscriptionId: String?) { + val subId = getSubscriptionId(subscriptionId) + val serverList = decodeServerList(subId) + + // Remove all servers in the list + serverList.forEach { guid -> + if (getSelectServer() == guid) { + mainStorage.remove(KEY_SELECTED_SERVER) + } + profileFullStorage.remove(guid) + serverAffStorage.remove(guid) } + + serverList.clear() + encodeServerList(serverList, subId) + + // Legacy code - keep for reference during migration + /* profileFullStorage.allKeys()?.forEach { key -> decodeServerConfig(key)?.let { config -> if (config.subscriptionId == subid) { @@ -178,6 +293,7 @@ object MmkvManager { } } } + */ } /** @@ -292,6 +408,10 @@ object MmkvManager { //region Subscriptions + private fun getSubscriptionId(subscriptionId: String?):String { + return subscriptionId?.ifEmpty { DEFAULT_SUBSCRIPTION_ID } ?: DEFAULT_SUBSCRIPTION_ID + } + /** * Initializes the subscription list. */ @@ -331,6 +451,11 @@ object MmkvManager { * @param subid The subscription ID. */ fun removeSubscription(subid: String) { + // Protect default subscription from being deleted + if (subid == DEFAULT_SUBSCRIPTION_ID) { + return + } + subStorage.remove(subid) val subsList = decodeSubsList() subsList.remove(subid) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt index 46fdb412..f62f0003 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt @@ -7,20 +7,24 @@ import android.util.Log import androidx.appcompat.app.AppCompatDelegate import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig.ANG_PACKAGE +import com.v2ray.ang.AppConfig.DEFAULT_SUBSCRIPTION_ID import com.v2ray.ang.AppConfig.GEOIP_PRIVATE import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE import com.v2ray.ang.AppConfig.TAG_DIRECT import com.v2ray.ang.AppConfig.VPN +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.RulesetItem +import com.v2ray.ang.dto.SubscriptionItem +import com.v2ray.ang.dto.V2rayConfig import com.v2ray.ang.enums.EConfigType 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.decodeAllServerList import com.v2ray.ang.handler.MmkvManager.decodeServerConfig -import com.v2ray.ang.handler.MmkvManager.decodeServerList +import com.v2ray.ang.handler.MmkvManager.decodeSubsList +import com.v2ray.ang.handler.MmkvManager.decodeSubscription +import com.v2ray.ang.handler.MmkvManager.encodeSubscription import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.Utils import java.io.File @@ -30,11 +34,19 @@ import java.util.Locale object SettingsManager { + fun initApp(context: Context) { + ensureDefaultSettings() + ensureDefaultSubscription() + initRoutingRulesets(context) + migrateServerListToSubscriptions() + migrateHysteria2PinSHA256() + } + /** * Initialize routing rulesets. * @param context The application context. */ - fun initRoutingRulesets(context: Context) { + private fun initRoutingRulesets(context: Context) { val exist = MmkvManager.decodeRoutingRulesets() if (exist.isNullOrEmpty()) { val rulesetList = getPresetRoutingRulesets(context) @@ -222,14 +234,10 @@ object SettingsManager { if (remarks.isNullOrEmpty()) { return null } - val serverList = decodeServerList() - for (guid in serverList) { - val profile = decodeServerConfig(guid) - if (profile != null && profile.remarks == remarks) { - return profile - } - } - return null + val serverList = decodeAllServerList() + return serverList + .mapNotNull { guid -> decodeServerConfig(guid) } + .firstOrNull { it.remarks == remarks } } /** @@ -400,7 +408,7 @@ object SettingsManager { /** * Ensure default settings are present in MMKV. */ - fun ensureDefaultSettings() { + private fun ensureDefaultSettings() { // Write defaults in the exact order requested by the user ensureDefaultValue(AppConfig.PREF_MODE, AppConfig.VPN) ensureDefaultValue(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN) @@ -424,14 +432,14 @@ object SettingsManager { } } - fun migrateHysteria2PinSHA256() { + private fun migrateHysteria2PinSHA256() { // Check if migration has already been done val migrationKey = "hysteria2_pin_sha256_migrated" if (MmkvManager.decodeSettingsBool(migrationKey, false)) { return } - val serverList = decodeServerList() + val serverList = decodeAllServerList() for (guid in serverList) { val profile = decodeServerConfig(guid) ?: continue @@ -449,4 +457,76 @@ object SettingsManager { MmkvManager.encodeSettings(migrationKey, true) } + /** + * Migrates server list from legacy KEY_ANG_CONFIGS to subscription-based storage. + * This method should be called once during app initialization after the storage structure change. + * Servers are grouped by their subscriptionId into respective subscription's serverList. + * Servers without subscription are moved to the default subscription. + * After migration, KEY_ANG_CONFIGS is removed. + */ + private fun migrateServerListToSubscriptions() { + // Check if migration has already been done + val migrationKey = "server_list_to_subscriptions_migrated" + if (MmkvManager.decodeSettingsBool(migrationKey, false)) { + return + } + + // Ensure default subscription exists before migration + ensureDefaultSubscription() + + // Read existing server list from legacy KEY_ANG_CONFIGS + val oldJson = MmkvManager.readLegacyServerList() + if (oldJson.isNullOrBlank()) { + // No data to migrate, mark as done + MmkvManager.encodeSettings(migrationKey, true) + return + } + + val guids = JsonUtil.fromJson(oldJson, Array::class.java) ?: run { + MmkvManager.encodeSettings(migrationKey, true) + return + } + + val subscriptionServerMap = mutableMapOf>() + + // Group servers by subscription (use default subscription for empty subscriptionId) + guids.forEach { guid -> + val config = decodeServerConfig(guid) ?: return@forEach + val subId = config.subscriptionId.ifEmpty { DEFAULT_SUBSCRIPTION_ID } + + subscriptionServerMap.getOrPut(subId) { mutableListOf() }.add(guid) + } + + // Update each subscription's serverList (including default subscription) + subscriptionServerMap.forEach { (subId, serverGuids) -> + MmkvManager.encodeServerList(serverGuids, subId) + } + + // Remove legacy KEY_ANG_CONFIGS data + // MmkvManager.removeLegacyServerListKey() + + // Mark migration as complete + MmkvManager.encodeSettings(migrationKey, true) + } + + /** + * Ensures the default subscription exists for ungrouped servers. + * This subscription is used internally to store servers without a subscription. + * Made public for migration in SettingsManager. + */ + private fun ensureDefaultSubscription() { + if (decodeSubscription(DEFAULT_SUBSCRIPTION_ID) == null) { + val defaultSub = SubscriptionItem( + remarks = "Default", + ) + encodeSubscription(DEFAULT_SUBSCRIPTION_ID, defaultSub) + + // Move top + val subsList = decodeSubsList() + if (subsList.count() > 1) { + swapSubscriptions(0, subsList.count() - 1) + } + } + } + } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt index 2f9b9a54..e2c7c7fb 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt @@ -6,8 +6,6 @@ import android.util.Log import com.google.gson.JsonArray import com.v2ray.ang.AppConfig import com.v2ray.ang.dto.ConfigResult -import com.v2ray.ang.enums.EConfigType -import com.v2ray.ang.enums.NetworkType import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.V2rayConfig @@ -15,6 +13,8 @@ import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean +import com.v2ray.ang.enums.EConfigType +import com.v2ray.ang.enums.NetworkType import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.extension.nullIfBlank import com.v2ray.ang.fmt.HttpFmt @@ -139,7 +139,7 @@ object V2rayConfigManager { private fun getV2rayGroupConfig(context: Context, guid: String, config: ProfileItem): ConfigResult { val result = ConfigResult(false) - val serverList = MmkvManager.decodeServerList() + val serverList = MmkvManager.decodeAllServerList() val configList = serverList .mapNotNull { id -> MmkvManager.decodeServerConfig(id) } .filter { profile -> diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt index 4f29b5d4..4ca88424 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt @@ -18,6 +18,7 @@ import com.v2ray.ang.extension.toastError import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsChangeManager +import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.WebDavManager import com.v2ray.ang.util.ZipUtil import kotlinx.coroutines.Dispatchers @@ -124,6 +125,8 @@ class BackupActivity : HelperBaseActivity() { val count = MMKV.restoreAllFromDirectory(backupDir) SettingsChangeManager.makeSetupGroupTab() SettingsChangeManager.makeRestartService() + + SettingsManager.initApp(this) return count > 0 } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/GroupServerFragment.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/GroupServerFragment.kt index 988c3746..e7023721 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/GroupServerFragment.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/GroupServerFragment.kt @@ -164,6 +164,7 @@ class GroupServerFragment : BaseFragment() { val intent = Intent().putExtra("guid", guid) .putExtra("isRunning", mainViewModel.isRunning.value) .putExtra("createConfigType", profile.configType.value) + .putExtra("subscriptionId", subId) when (profile.configType) { EConfigType.CUSTOM -> { ownerActivity.startActivity(intent.setClass(ownerActivity, ServerCustomConfigActivity::class.java)) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt index 8169f199..5c7db83e 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt @@ -145,7 +145,7 @@ class SubEditActivity : BaseActivity() { del_config = menu.findItem(R.id.del_config) save_config = menu.findItem(R.id.save_config) - if (editSubId.isEmpty()) { + if (editSubId.isEmpty() || editSubId == AppConfig.DEFAULT_SUBSCRIPTION_ID) { del_config?.isVisible = false } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt index 0c482499..164fba6a 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt @@ -5,7 +5,9 @@ import android.text.TextUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView +import com.v2ray.ang.AppConfig import com.v2ray.ang.contracts.BaseAdapterListener import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding import com.v2ray.ang.helper.ItemTouchHelperAdapter @@ -37,6 +39,7 @@ class SubSettingRecyclerAdapter( holder.itemSubSettingBinding.layoutRemove.setOnClickListener { adapterListener?.onRemove(subId, position) } + holder.itemSubSettingBinding.layoutRemove.isVisible = subId != AppConfig.DEFAULT_SUBSCRIPTION_ID holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked -> if (!it.isPressed) return@setOnCheckedChangeListener diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt index 89f4fbe1..41f5430b 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt @@ -30,7 +30,7 @@ class TaskerActivity : BaseActivity() { lstData.add("Default") lstGuid.add(AppConfig.TASKER_DEFAULT_GUID) - MmkvManager.decodeServerList().forEach { key -> + MmkvManager.decodeAllServerList().forEach { key -> MmkvManager.decodeServerConfig(key)?.let { config -> lstData.add(config.remarks) lstGuid.add(key) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt index 0b0de831..6b17f25b 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt @@ -35,7 +35,7 @@ import kotlinx.coroutines.withContext import java.util.Collections class MainViewModel(application: Application) : AndroidViewModel(application) { - private var serverList = MmkvManager.decodeServerList() + private var serverList = mutableListOf() // MmkvManager.decodeServerList() var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty() //var keywordFilter: String = MmkvManager.MmkvManager.decodeSettingsString(AppConfig.CACHE_KEYWORD_FILTER, "")?:"" @@ -69,10 +69,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } /** - * Reloads the server list. + * Reloads the server list based on current subscription filter. */ fun reloadServerList() { - serverList = MmkvManager.decodeServerList() + serverList = if (subscriptionId.isEmpty()) { + MmkvManager.decodeAllServerList() + } else { + MmkvManager.decodeServerList(subscriptionId) + } + updateCache() updateListAction.value = -1 } @@ -129,14 +134,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { */ fun swapServer(fromPosition: Int, toPosition: Int) { if (subscriptionId.isEmpty()) { - Collections.swap(serverList, fromPosition, toPosition) - } else { - val fromPosition2 = serverList.indexOf(serversCache[fromPosition].guid) - val toPosition2 = serverList.indexOf(serversCache[toPosition].guid) - Collections.swap(serverList, fromPosition2, toPosition2) + return } + + Collections.swap(serverList, fromPosition, toPosition) Collections.swap(serversCache, fromPosition, toPosition) - MmkvManager.encodeServerList(serverList) + + MmkvManager.encodeServerList(serverList, subscriptionId) } /** @@ -160,9 +164,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { // MmkvManager.encodeServerConfig(guid, config) // } - if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) { - continue - } +// if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) { +// continue +// } if (keywordFilter.isEmpty() || profile.remarks.lowercase().contains(keywordFilter.lowercase())) { serversCache.add(ServersCache(guid, profile)) @@ -279,14 +283,21 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } val groups = mutableListOf() - groups.add( - GroupMapItem( - id = "", - remarks = context.getString(R.string.filter_config_all) + if (subscriptions.count() > 1) { + groups.add( + GroupMapItem( + id = "", + remarks = context.getString(R.string.filter_config_all) + ) ) - ) + } subscriptions.forEach { sub -> - groups.add(GroupMapItem(id = sub.guid, remarks = sub.subscription.remarks)) + groups.add( + GroupMapItem( + id = sub.guid, + remarks = sub.subscription.remarks + ) + ) } return groups } @@ -368,24 +379,38 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { * Sorts servers by their test results. */ fun sortByTestResults() { + if (subscriptionId.isEmpty()) { + MmkvManager.decodeSubsList().forEach { guid -> + sortByTestResultsForSub(guid) + } + } else { + sortByTestResultsForSub(subscriptionId) + } + } + + /** + * Sorts servers by their test results for a specific subscription. + * @param subId The subscription ID to sort servers for. + */ + private fun sortByTestResultsForSub(subId: String) { data class ServerDelay(var guid: String, var testDelayMillis: Long) val serverDelays = mutableListOf() - val serverList = MmkvManager.decodeServerList() - serverList.forEach { key -> + val serverListToSort = MmkvManager.decodeServerList(subId) + + serverListToSort.forEach { key -> val delay = MmkvManager.decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L serverDelays.add(ServerDelay(key, if (delay <= 0L) 999999 else delay)) } serverDelays.sortBy { it.testDelayMillis } - serverDelays.forEach { - serverList.remove(it.guid) - serverList.add(it.guid) - } + val sortedServerList = serverDelays.map { it.guid }.toMutableList() - MmkvManager.encodeServerList(serverList) + // Save the sorted list for this subscription + MmkvManager.encodeServerList(sortedServerList, subId) } + /** * Initializes assets. * @param assets The asset manager.