Move date restore view state into ViewModel to survive configuration changes

This commit is contained in:
Torsten Grote 2020-01-09 14:01:26 -03:00
parent 5632f11878
commit 1924db7779
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
4 changed files with 88 additions and 49 deletions

View file

@ -55,6 +55,9 @@ class InstallProgressFragment : Fragment() {
}
private fun onInstallResult(installResult: InstallResult) {
// skip this screen, if there are no apps to install
if (installResult.isEmpty()) viewModel.onNextClicked()
val result = installResult.filterValues { it.status != QUEUED }
val position = layoutManager.findFirstVisibleItemPosition()
adapter.update(result.values)

View file

@ -9,20 +9,20 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.getAppName
import com.stevesoltys.seedvault.restore.AppRestoreStatus.IN_PROGRESS
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
import com.stevesoltys.seedvault.restore.RestoreProgressAdapter.PackageViewHolder
import java.util.*
internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
private val items = LinkedList<AppRestoreResult>().apply {
add(AppRestoreResult(MAGIC_PACKAGE_MANAGER, true))
}
private var isComplete = false
private val items = LinkedList<AppRestoreResult>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PackageViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item_app_status, parent, false)
@ -32,28 +32,30 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: PackageViewHolder, position: Int) {
holder.bind(items[position], position == 0)
holder.bind(items[position])
}
fun getLatest(): AppRestoreResult {
return items[0]
fun update(newItems: LinkedList<AppRestoreResult>) {
val diffResult = DiffUtil.calculateDiff(Diff(items, newItems))
items.clear()
items.addAll(newItems)
diffResult.dispatchUpdatesTo(this)
}
fun setLatestFailed() {
items[0] = AppRestoreResult(items[0].packageName, false)
notifyItemChanged(0, items[0])
}
private class Diff(
private val oldItems: LinkedList<AppRestoreResult>,
private val newItems: LinkedList<AppRestoreResult>) : DiffUtil.Callback() {
fun add(item: AppRestoreResult) {
items.addFirst(item)
notifyItemInserted(0)
notifyItemRangeChanged(1, items.size - 1)
}
override fun getOldListSize() = oldItems.size
override fun getNewListSize() = newItems.size
fun setComplete(): List<String> {
isComplete = true
notifyItemChanged(0)
return items.map { it.packageName }
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldItems[oldItemPosition].packageName == newItems[newItemPosition].packageName
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldItems[oldItemPosition] == newItems[newItemPosition]
}
}
inner class PackageViewHolder(v: View) : ViewHolder(v) {
@ -65,7 +67,7 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
private val appStatus: ImageView = v.findViewById(R.id.appStatus)
private val progressBar: ProgressBar = v.findViewById(R.id.progressBar)
fun bind(item: AppRestoreResult, isLatest: Boolean) {
fun bind(item: AppRestoreResult) {
if (item.packageName == MAGIC_PACKAGE_MANAGER) {
appIcon.setImageResource(R.drawable.ic_launcher_default)
appName.text = context.getString(R.string.restore_magic_package)
@ -77,11 +79,14 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
}
appName.text = getAppName(pm, item.packageName)
}
if (isLatest && !isComplete) {
if (item.status == IN_PROGRESS) {
appStatus.visibility = INVISIBLE
progressBar.visibility = VISIBLE
} else {
appStatus.setImageResource(if (item.success) R.drawable.ic_check_green else R.drawable.ic_cancel_red)
appStatus.setImageResource(
if (item.status == SUCCEEDED) R.drawable.ic_check_green
else R.drawable.ic_cancel_red
)
appStatus.visibility = VISIBLE
progressBar.visibility = INVISIBLE
}
@ -91,4 +96,6 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
}
data class AppRestoreResult(val packageName: String, val success: Boolean)
internal enum class AppRestoreStatus { IN_PROGRESS, SUCCEEDED, FAILED }
internal data class AppRestoreResult(val packageName: String, val status: AppRestoreStatus)

View file

@ -51,28 +51,11 @@ class RestoreProgressFragment : Fragment() {
backupNameView.text = restorableBackup.name
})
viewModel.restoreProgress.observe(this, Observer { currentPackage ->
stayScrolledAtTop {
val latest = adapter.getLatest()
if (viewModel.isFailedPackage(latest.packageName)) {
adapter.setLatestFailed()
}
adapter.add(AppRestoreResult(currentPackage, true))
}
viewModel.restoreProgress.observe(this, Observer { list ->
stayScrolledAtTop { adapter.update(list) }
})
viewModel.restoreBackupResult.observe(this, Observer { finished ->
val seenPackages = adapter.setComplete()
stayScrolledAtTop {
// add missing packages as failed
val restorableBackup = viewModel.chosenRestorableBackup.value!!
val expectedPackages = restorableBackup.packageMetadataMap.keys
expectedPackages.removeAll(seenPackages)
for (packageName: String in expectedPackages) {
adapter.add(AppRestoreResult(packageName, false))
}
}
button.isEnabled = true
if (finished.hasError()) {
backupNameView.text = finished.errorMsg

View file

@ -15,8 +15,10 @@ import androidx.lifecycle.Transformations.switchMap
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.stevesoltys.seedvault.BackupMonitor
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.restore.AppRestoreStatus.*
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
import com.stevesoltys.seedvault.settings.SettingsManager
@ -35,6 +37,7 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import java.util.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@ -73,8 +76,12 @@ internal class RestoreViewModel(
private val mNextButtonEnabled = MutableLiveData<Boolean>().apply { value = false }
internal val nextButtonEnabled: LiveData<Boolean> = mNextButtonEnabled
private val mRestoreProgress = MutableLiveData<String>()
internal val restoreProgress: LiveData<String> get() = mRestoreProgress
private val mRestoreProgress = MutableLiveData<LinkedList<AppRestoreResult>>().apply {
value = LinkedList<AppRestoreResult>().apply {
add(AppRestoreResult(MAGIC_PACKAGE_MANAGER, IN_PROGRESS))
}
}
internal val restoreProgress: LiveData<LinkedList<AppRestoreResult>> get() = mRestoreProgress
private val mRestoreBackupResult = MutableLiveData<RestoreBackupResult>()
internal val restoreBackupResult: LiveData<RestoreBackupResult> get() = mRestoreBackupResult
@ -164,7 +171,46 @@ internal class RestoreViewModel(
}
}
fun isFailedPackage(packageName: String) = restoreCoordinator.isFailedPackage(packageName)
@WorkerThread
// this should be called one package at a time and never concurrently for different packages
private fun onRestoreStarted(packageName: String) {
// list is never null and always has at least one package
val list = mRestoreProgress.value!!
// check previous package first and change status
updateLatestPackage(list)
// add current package
list.addFirst(AppRestoreResult(packageName, IN_PROGRESS))
mRestoreProgress.postValue(list)
}
private fun updateLatestPackage(list: LinkedList<AppRestoreResult>) {
val latestResult = list[0]
if (restoreCoordinator.isFailedPackage(latestResult.packageName)) {
list[0] = latestResult.copy(status = FAILED)
} else {
list[0] = latestResult.copy(status = SUCCEEDED)
}
}
@WorkerThread
private fun onRestoreComplete(result: RestoreBackupResult) {
// update status of latest package
val list = mRestoreProgress.value!!
updateLatestPackage(list)
// add missing packages as failed
val seenPackages = list.map { it.packageName }
val restorableBackup = chosenRestorableBackup.value!!
val expectedPackages = restorableBackup.packageMetadataMap.keys
expectedPackages.removeAll(seenPackages)
for (packageName: String in expectedPackages) {
list.add(AppRestoreResult(packageName, FAILED))
}
mRestoreProgress.postValue(list)
mRestoreBackupResult.postValue(result)
}
override fun onCleared() {
super.onCleared()
@ -189,7 +235,7 @@ internal class RestoreViewModel(
* the current device. If no applicable datasets exist, restoreSets will be null.
*/
override fun restoreSetsAvailable(restoreSets: Array<out RestoreSet>?) {
check (continuation != null) { "Getting restore sets without continuation" }
check(continuation != null) { "Getting restore sets without continuation" }
val result = if (restoreSets == null || restoreSets.isEmpty()) {
RestoreSetResult(app.getString(R.string.restore_set_empty_result))
@ -241,7 +287,7 @@ internal class RestoreViewModel(
*/
override fun onUpdate(nowBeingRestored: Int, currentPackage: String) {
// nowBeingRestored reporting is buggy, so don't use it
mRestoreProgress.postValue(currentPackage)
onRestoreStarted(currentPackage)
}
/**
@ -255,7 +301,7 @@ internal class RestoreViewModel(
if (result == 0) null
else app.getString(R.string.restore_finished_error)
)
mRestoreBackupResult.postValue(restoreResult)
onRestoreComplete(restoreResult)
closeSession()
}