Factor out app restore selection code into new AppSelectionManager
This commit is contained in:
parent
332387fd58
commit
f5fb9ffffa
2 changed files with 169 additions and 112 deletions
|
@ -0,0 +1,161 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.stevesoltys.seedvault.restore
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
|
import com.stevesoltys.seedvault.R
|
||||||
|
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
|
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
|
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SYSTEM
|
||||||
|
import com.stevesoltys.seedvault.ui.systemData
|
||||||
|
import com.stevesoltys.seedvault.worker.FILE_BACKUP_ICONS
|
||||||
|
import com.stevesoltys.seedvault.worker.IconManager
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
internal class SelectedAppsState(
|
||||||
|
val apps: List<SelectableAppItem>,
|
||||||
|
val allSelected: Boolean,
|
||||||
|
val iconsLoaded: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val TAG = AppSelectionManager::class.simpleName
|
||||||
|
|
||||||
|
internal class AppSelectionManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val pluginManager: StoragePluginManager,
|
||||||
|
private val iconManager: IconManager,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val initialState = SelectedAppsState(
|
||||||
|
emptyList(),
|
||||||
|
allSelected = true,
|
||||||
|
iconsLoaded = false,
|
||||||
|
)
|
||||||
|
private val selectedApps = MutableStateFlow(initialState)
|
||||||
|
val selectedAppsFlow = selectedApps.asStateFlow()
|
||||||
|
val selectedAppsLiveData: LiveData<SelectedAppsState> = selectedApps.asLiveData()
|
||||||
|
|
||||||
|
fun onRestoreSetChosen(restorableBackup: RestorableBackup) {
|
||||||
|
// filter and sort app items for display
|
||||||
|
val items = restorableBackup.packageMetadataMap.mapNotNull { (packageName, metadata) ->
|
||||||
|
if (metadata.time == 0L && !metadata.hasApk()) null
|
||||||
|
else if (metadata.isInternalSystem) null
|
||||||
|
else SelectableAppItem(packageName, metadata, true)
|
||||||
|
}.sortedBy {
|
||||||
|
it.name.lowercase(Locale.getDefault())
|
||||||
|
}.toMutableList()
|
||||||
|
val systemDataItems = systemData.mapNotNull { (packageName, data) ->
|
||||||
|
val metadata = restorableBackup.packageMetadataMap[packageName]
|
||||||
|
?: return@mapNotNull null
|
||||||
|
if (metadata.time == 0L && !metadata.hasApk()) return@mapNotNull null
|
||||||
|
val name = context.getString(data.nameRes)
|
||||||
|
SelectableAppItem(packageName, metadata.copy(name = name), true)
|
||||||
|
}
|
||||||
|
val systemItem = SelectableAppItem(
|
||||||
|
packageName = PACKAGE_NAME_SYSTEM,
|
||||||
|
metadata = PackageMetadata(
|
||||||
|
time = restorableBackup.packageMetadataMap.values.maxOf {
|
||||||
|
if (it.system) it.time else -1
|
||||||
|
},
|
||||||
|
system = true,
|
||||||
|
name = context.getString(R.string.backup_system_apps),
|
||||||
|
),
|
||||||
|
selected = true,
|
||||||
|
)
|
||||||
|
items.add(0, systemItem)
|
||||||
|
items.addAll(0, systemDataItems)
|
||||||
|
selectedApps.value =
|
||||||
|
SelectedAppsState(apps = items, allSelected = true, iconsLoaded = false)
|
||||||
|
// download icons
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
val plugin = pluginManager.appPlugin
|
||||||
|
val token = restorableBackup.token
|
||||||
|
val packagesWithIcons = try {
|
||||||
|
plugin.getInputStream(token, FILE_BACKUP_ICONS).use {
|
||||||
|
iconManager.downloadIcons(restorableBackup.version, token, it)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error loading icons:", e)
|
||||||
|
emptySet()
|
||||||
|
} + systemData.keys + setOf(PACKAGE_NAME_SYSTEM) // special apps have built-in icons
|
||||||
|
// update state, so it knows that icons have loaded
|
||||||
|
val updatedItems = items.map { item ->
|
||||||
|
item.copy(hasIcon = item.packageName in packagesWithIcons)
|
||||||
|
}
|
||||||
|
selectedApps.value =
|
||||||
|
SelectedAppsState(updatedItems, allSelected = true, iconsLoaded = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCheckAllAppsClicked() {
|
||||||
|
val apps = selectedApps.value.apps
|
||||||
|
val allSelected = apps.all { it.selected }
|
||||||
|
if (allSelected) {
|
||||||
|
// unselect all
|
||||||
|
val newApps = apps.map { if (it.selected) it.copy(selected = false) else it }
|
||||||
|
selectedApps.value = SelectedAppsState(newApps, false, iconsLoaded = true)
|
||||||
|
} else {
|
||||||
|
// select all
|
||||||
|
val newApps = apps.map { if (!it.selected) it.copy(selected = true) else it }
|
||||||
|
selectedApps.value = SelectedAppsState(newApps, true, iconsLoaded = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAppSelected(item: SelectableAppItem) {
|
||||||
|
val apps = selectedApps.value.apps.toMutableList()
|
||||||
|
val iterator = apps.listIterator()
|
||||||
|
var allSelected = true
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val app = iterator.next()
|
||||||
|
if (app.packageName == item.packageName) {
|
||||||
|
iterator.set(item.copy(selected = !item.selected))
|
||||||
|
allSelected = allSelected && !item.selected
|
||||||
|
} else {
|
||||||
|
allSelected = allSelected && app.selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedApps.value = SelectedAppsState(apps, allSelected, iconsLoaded = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAppSelectionFinished(backup: RestorableBackup): RestorableBackup {
|
||||||
|
// map packages names to selection state
|
||||||
|
val apps = selectedApps.value.apps.associate {
|
||||||
|
Pair(it.packageName, it.selected)
|
||||||
|
}
|
||||||
|
// filter out unselected packages
|
||||||
|
// Attention: This code is complicated and hard to test, proceed with plenty of care!
|
||||||
|
val restoreSystemApps = apps[PACKAGE_NAME_SYSTEM] != false
|
||||||
|
val packages = backup.packageMetadataMap.filter { (packageName, metadata) ->
|
||||||
|
val isSelected = apps[packageName]
|
||||||
|
@Suppress("IfThenToElvis") // the code is more readable like this
|
||||||
|
if (isSelected == null) { // was not in list
|
||||||
|
if (packageName == MAGIC_PACKAGE_MANAGER) true // @pm@ is essential for restore
|
||||||
|
// internal system apps were not in the list and are controlled by meta item,
|
||||||
|
// so allow them only if meta item was selected
|
||||||
|
else if (metadata.isInternalSystem) restoreSystemApps
|
||||||
|
else true // non-system packages that weren't found, won't get filtered
|
||||||
|
} else { // was in list and either selected or not
|
||||||
|
isSelected
|
||||||
|
}
|
||||||
|
} as PackageMetadataMap
|
||||||
|
// replace original chosen backup with unselected packages removed
|
||||||
|
return backup.copy(
|
||||||
|
backupMetadata = backup.backupMetadata.copy(packageMetadataMap = packages),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -29,8 +29,6 @@ import com.stevesoltys.seedvault.BackupMonitor
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
||||||
|
@ -66,7 +64,6 @@ import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SYSTEM
|
||||||
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
||||||
import com.stevesoltys.seedvault.ui.notification.getAppName
|
import com.stevesoltys.seedvault.ui.notification.getAppName
|
||||||
import com.stevesoltys.seedvault.ui.systemData
|
import com.stevesoltys.seedvault.ui.systemData
|
||||||
import com.stevesoltys.seedvault.worker.FILE_BACKUP_ICONS
|
|
||||||
import com.stevesoltys.seedvault.worker.IconManager
|
import com.stevesoltys.seedvault.worker.IconManager
|
||||||
import com.stevesoltys.seedvault.worker.NUM_PACKAGES_PER_TRANSACTION
|
import com.stevesoltys.seedvault.worker.NUM_PACKAGES_PER_TRANSACTION
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
@ -82,18 +79,11 @@ import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_TIMESTA
|
||||||
import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_USER_ID
|
import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_USER_ID
|
||||||
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
|
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
private val TAG = RestoreViewModel::class.java.simpleName
|
private val TAG = RestoreViewModel::class.java.simpleName
|
||||||
|
|
||||||
internal const val PACKAGES_PER_CHUNK = NUM_PACKAGES_PER_TRANSACTION
|
internal const val PACKAGES_PER_CHUNK = NUM_PACKAGES_PER_TRANSACTION
|
||||||
|
|
||||||
internal class SelectedAppsState(
|
|
||||||
val apps: List<SelectableAppItem>,
|
|
||||||
val allSelected: Boolean,
|
|
||||||
val iconsLoaded: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
internal class RestoreViewModel(
|
internal class RestoreViewModel(
|
||||||
app: Application,
|
app: Application,
|
||||||
settingsManager: SettingsManager,
|
settingsManager: SettingsManager,
|
||||||
|
@ -112,6 +102,8 @@ internal class RestoreViewModel(
|
||||||
|
|
||||||
private var session: IRestoreSession? = null
|
private var session: IRestoreSession? = null
|
||||||
private val monitor = BackupMonitor()
|
private val monitor = BackupMonitor()
|
||||||
|
private val appSelectionManager =
|
||||||
|
AppSelectionManager(app, pluginManager, iconManager, viewModelScope)
|
||||||
|
|
||||||
private val mDisplayFragment = MutableLiveEvent<DisplayFragment>()
|
private val mDisplayFragment = MutableLiveEvent<DisplayFragment>()
|
||||||
internal val displayFragment: LiveEvent<DisplayFragment> = mDisplayFragment
|
internal val displayFragment: LiveEvent<DisplayFragment> = mDisplayFragment
|
||||||
|
@ -122,8 +114,8 @@ internal class RestoreViewModel(
|
||||||
private val mChosenRestorableBackup = MutableLiveData<RestorableBackup>()
|
private val mChosenRestorableBackup = MutableLiveData<RestorableBackup>()
|
||||||
internal val chosenRestorableBackup: LiveData<RestorableBackup> get() = mChosenRestorableBackup
|
internal val chosenRestorableBackup: LiveData<RestorableBackup> get() = mChosenRestorableBackup
|
||||||
|
|
||||||
private val mSelectedApps = MutableLiveData<SelectedAppsState>()
|
internal val selectedApps: LiveData<SelectedAppsState> =
|
||||||
internal val selectedApps: LiveData<SelectedAppsState> get() = mSelectedApps
|
appSelectionManager.selectedAppsLiveData
|
||||||
|
|
||||||
internal val installResult: LiveData<InstallResult> =
|
internal val installResult: LiveData<InstallResult> =
|
||||||
mChosenRestorableBackup.switchMap { backup ->
|
mChosenRestorableBackup.switchMap { backup ->
|
||||||
|
@ -185,56 +177,7 @@ internal class RestoreViewModel(
|
||||||
|
|
||||||
override fun onRestorableBackupClicked(restorableBackup: RestorableBackup) {
|
override fun onRestorableBackupClicked(restorableBackup: RestorableBackup) {
|
||||||
mChosenRestorableBackup.value = restorableBackup
|
mChosenRestorableBackup.value = restorableBackup
|
||||||
// filter and sort app items for display
|
appSelectionManager.onRestoreSetChosen(restorableBackup)
|
||||||
val items = restorableBackup.packageMetadataMap.mapNotNull { (packageName, metadata) ->
|
|
||||||
if (metadata.time == 0L && !metadata.hasApk()) null
|
|
||||||
else if (metadata.isInternalSystem) null
|
|
||||||
else SelectableAppItem(packageName, metadata, true)
|
|
||||||
}.sortedBy {
|
|
||||||
it.name.lowercase(Locale.getDefault())
|
|
||||||
}.toMutableList()
|
|
||||||
val systemDataItems = systemData.mapNotNull { (packageName, data) ->
|
|
||||||
val metadata = restorableBackup.packageMetadataMap[packageName]
|
|
||||||
?: return@mapNotNull null
|
|
||||||
if (metadata.time == 0L && !metadata.hasApk()) return@mapNotNull null
|
|
||||||
val name = app.getString(data.nameRes)
|
|
||||||
SelectableAppItem(packageName, metadata.copy(name = name), true)
|
|
||||||
}
|
|
||||||
val systemItem = SelectableAppItem(
|
|
||||||
packageName = PACKAGE_NAME_SYSTEM,
|
|
||||||
metadata = PackageMetadata(
|
|
||||||
time = restorableBackup.packageMetadataMap.values.maxOf {
|
|
||||||
if (it.system) it.time else -1
|
|
||||||
},
|
|
||||||
system = true,
|
|
||||||
name = app.getString(R.string.backup_system_apps),
|
|
||||||
),
|
|
||||||
selected = true,
|
|
||||||
)
|
|
||||||
items.add(0, systemItem)
|
|
||||||
items.addAll(0, systemDataItems)
|
|
||||||
mSelectedApps.value =
|
|
||||||
SelectedAppsState(apps = items, allSelected = true, iconsLoaded = false)
|
|
||||||
// download icons
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
val plugin = pluginManager.appPlugin
|
|
||||||
val token = restorableBackup.token
|
|
||||||
val packagesWithIcons = try {
|
|
||||||
plugin.getInputStream(token, FILE_BACKUP_ICONS).use {
|
|
||||||
iconManager.downloadIcons(restorableBackup.version, token, it)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error loading icons:", e)
|
|
||||||
emptySet()
|
|
||||||
} + systemData.keys + setOf(PACKAGE_NAME_SYSTEM)
|
|
||||||
// update state, so it knows that icons have loaded
|
|
||||||
val updatedItems = items.map { item ->
|
|
||||||
item.copy(hasIcon = item.packageName in packagesWithIcons)
|
|
||||||
}
|
|
||||||
val newState =
|
|
||||||
SelectedAppsState(updatedItems, allSelected = true, iconsLoaded = true)
|
|
||||||
mSelectedApps.postValue(newState)
|
|
||||||
}
|
|
||||||
mDisplayFragment.setEvent(SELECT_APPS)
|
mDisplayFragment.setEvent(SELECT_APPS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,60 +193,13 @@ internal class RestoreViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onCheckAllAppsClicked() {
|
fun onCheckAllAppsClicked() = appSelectionManager.onCheckAllAppsClicked()
|
||||||
val apps = selectedApps.value?.apps ?: return
|
fun onAppSelected(item: SelectableAppItem) = appSelectionManager.onAppSelected(item)
|
||||||
val allSelected = apps.all { it.selected }
|
|
||||||
if (allSelected) {
|
|
||||||
// unselect all
|
|
||||||
val newApps = apps.map { if (it.selected) it.copy(selected = false) else it }
|
|
||||||
mSelectedApps.value = SelectedAppsState(newApps, false, iconsLoaded = true)
|
|
||||||
} else {
|
|
||||||
// select all
|
|
||||||
val newApps = apps.map { if (!it.selected) it.copy(selected = true) else it }
|
|
||||||
mSelectedApps.value = SelectedAppsState(newApps, true, iconsLoaded = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAppSelected(item: SelectableAppItem) {
|
|
||||||
val apps = selectedApps.value?.apps?.toMutableList() ?: return
|
|
||||||
val iterator = apps.listIterator()
|
|
||||||
var allSelected = true
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
val app = iterator.next()
|
|
||||||
if (app.packageName == item.packageName) {
|
|
||||||
iterator.set(item.copy(selected = !item.selected))
|
|
||||||
allSelected = allSelected && !item.selected
|
|
||||||
} else {
|
|
||||||
allSelected = allSelected && app.selected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mSelectedApps.value = SelectedAppsState(apps, allSelected, iconsLoaded = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun onNextClickedAfterSelectingApps() {
|
internal fun onNextClickedAfterSelectingApps() {
|
||||||
val backup = chosenRestorableBackup.value ?: error("No chosen backup")
|
val backup = chosenRestorableBackup.value ?: error("No chosen backup")
|
||||||
// map packages names to selection state
|
|
||||||
val apps = selectedApps.value?.apps?.associate {
|
|
||||||
Pair(it.packageName, it.selected)
|
|
||||||
} ?: error("no selected apps")
|
|
||||||
// filter out unselected packages
|
|
||||||
// Attention: This code is complicated and hard to test, proceed with plenty of care!
|
|
||||||
val restoreSystemApps = apps[PACKAGE_NAME_SYSTEM] != false
|
|
||||||
val packages = backup.packageMetadataMap.filter { (packageName, metadata) ->
|
|
||||||
val isSelected = apps[packageName]
|
|
||||||
@Suppress("IfThenToElvis") // the code is more readable like this
|
|
||||||
if (isSelected == null) { // was not in list
|
|
||||||
// internal system apps were not in the list and are controlled by meta item
|
|
||||||
if (metadata.isInternalSystem) restoreSystemApps // only if allowed by meta item
|
|
||||||
else true // non-system packages that weren't found, won't get filtered
|
|
||||||
} else { // was in list and either selected or not
|
|
||||||
isSelected
|
|
||||||
}
|
|
||||||
} as PackageMetadataMap
|
|
||||||
// replace original chosen backup with unselected packages removed
|
// replace original chosen backup with unselected packages removed
|
||||||
mChosenRestorableBackup.value = backup.copy(
|
mChosenRestorableBackup.value = appSelectionManager.onAppSelectionFinished(backup)
|
||||||
backupMetadata = backup.backupMetadata.copy(packageMetadataMap = packages),
|
|
||||||
)
|
|
||||||
// tell UI to move to InstallFragment
|
// tell UI to move to InstallFragment
|
||||||
mDisplayFragment.setEvent(RESTORE_APPS)
|
mDisplayFragment.setEvent(RESTORE_APPS)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue