Preserve app picker state on Activity recreation (#5972)

* Preserve app picker selection on recreation

* Avoid reloading app picker data on recreation
This commit is contained in:
eliotcougar authored and GitHub committed 2026-07-31 09:59:33 +08:00
1 parent 070abd78ea
commit aacc8e091f
2 files changed
+48 -10

No files matched your search

@@ -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<List<AppInfo>>(emptyList())
val displayedApps: StateFlow<List<AppInfo>> = _displayedApps.asStateFlow()
private var appsAll: List<AppInfo> = emptyList()
private var allApps: List<AppInfo>? = null
private var currentQuery: String = ""
private var SelectedSnapshot: Set<String> = emptySet()
private var selectedSnapshot: Set<String> = emptySet()
private var initialized = false
private var isAppListLoading = false
fun initialize(initialSelected: Collection<String>) {
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<String> = _selectedPackages.value.sorted()
private fun applyFilter(query: String): List<AppInfo> {
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<AppInfo>): List<AppInfo> {
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
)
}
}
}
@@ -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<Application>())
viewModel.initialize(listOf("initial.package"))
viewModel.toggleApp("added.package")
viewModel.initialize(listOf("initial.package"))
assertEquals(
setOf("initial.package", "added.package"),
viewModel.selectedPackages.value
)
}
}