From aacc8e091f17c63cca3b634f55c805a0779ba4e2 Mon Sep 17 00:00:00 2001 From: eliotcougar Date: Fri, 31 Jul 2026 04:59:33 +0300 Subject: [PATCH] Preserve app picker state on Activity recreation (#5972) * Preserve app picker selection on recreation * Avoid reloading app picker data on recreation --- .../ang/ui/apppicker/AppPickerViewModel.kt | 35 +++++++++++++------ .../ui/apppicker/AppPickerViewModelTest.kt | 23 ++++++++++++ 2 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 V2rayNG/app/src/test/java/com/v2ray/ang/ui/apppicker/AppPickerViewModelTest.kt diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerViewModel.kt index b5ecdc53..b56c526e 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerViewModel.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/apppicker/AppPickerViewModel.kt @@ -8,6 +8,7 @@ import com.v2ray.ang.dto.AppInfo import com.v2ray.ang.ui.base.BaseViewModel import com.v2ray.ang.util.AppManagerUtil import com.v2ray.ang.util.LogUtil +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -26,28 +27,40 @@ class AppPickerViewModel(application: Application) : BaseViewModel(application) private val _displayedApps = MutableStateFlow>(emptyList()) val displayedApps: StateFlow> = _displayedApps.asStateFlow() - private var appsAll: List = emptyList() + private var allApps: List? = null private var currentQuery: String = "" - private var SelectedSnapshot: Set = emptySet() + private var selectedSnapshot: Set = emptySet() + private var initialized = false + private var isAppListLoading = false fun initialize(initialSelected: Collection) { + if (initialized) return + initialized = true _selectedPackages.value = initialSelected.toSet() } fun loadApps(context: Context) { + if (allApps != null || isAppListLoading) return + + val applicationContext = context.applicationContext + isAppListLoading = true launchLoading { try { - SelectedSnapshot = _selectedPackages.value.toSet() + selectedSnapshot = _selectedPackages.value val apps = withContext(Dispatchers.IO) { - val list = AppManagerUtil.loadNetworkAppList(context) - val special = createSpecialItemUnidentified(context) + val list = AppManagerUtil.loadNetworkAppList(applicationContext) + val special = createSpecialItemUnidentified(applicationContext) sortApps(list + special) } - appsAll = apps + allApps = apps _displayedApps.value = applyFilter(currentQuery) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { LogUtil.e("AppPickerViewModel", "Failed to load app list", e) toastError(R.string.toast_failure) + } finally { + isAppListLoading = false } } } @@ -81,16 +94,18 @@ class AppPickerViewModel(application: Application) : BaseViewModel(application) fun getSelectedPackages(): List = _selectedPackages.value.sorted() private fun applyFilter(query: String): List { - if (query.isBlank()) return appsAll + val apps = allApps ?: return emptyList() + if (query.isBlank()) return apps + val key = query.uppercase() - return appsAll.filter { + return apps.filter { it.appName.uppercase().contains(key) || it.packageName.uppercase().contains(key) } } private fun sortApps(apps: List): List { val collator = Collator.getInstance() - val snapshot = SelectedSnapshot + val snapshot = selectedSnapshot return apps.sortedWith { p1, p2 -> val p1Selected = snapshot.contains(p1.packageName) val p2Selected = snapshot.contains(p2.packageName) @@ -116,4 +131,4 @@ class AppPickerViewModel(application: Application) : BaseViewModel(application) isSelected = 0 ) } -} \ No newline at end of file +} diff --git a/V2rayNG/app/src/test/java/com/v2ray/ang/ui/apppicker/AppPickerViewModelTest.kt b/V2rayNG/app/src/test/java/com/v2ray/ang/ui/apppicker/AppPickerViewModelTest.kt new file mode 100644 index 00000000..27ff0c4a --- /dev/null +++ b/V2rayNG/app/src/test/java/com/v2ray/ang/ui/apppicker/AppPickerViewModelTest.kt @@ -0,0 +1,23 @@ +package com.v2ray.ang.ui.apppicker + +import android.app.Application +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mockito.kotlin.mock + +class AppPickerViewModelTest { + + @Test + fun initialize_preservesChangesAfterFirstCall() { + val viewModel = AppPickerViewModel(mock()) + + viewModel.initialize(listOf("initial.package")) + viewModel.toggleApp("added.package") + viewModel.initialize(listOf("initial.package")) + + assertEquals( + setOf("initial.package", "added.package"), + viewModel.selectedPackages.value + ) + } +}