Move date restore view state into ViewModel to survive configuration changes
This commit is contained in:
parent
5632f11878
commit
1924db7779
4 changed files with 88 additions and 49 deletions
|
@ -55,6 +55,9 @@ class InstallProgressFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onInstallResult(installResult: InstallResult) {
|
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 result = installResult.filterValues { it.status != QUEUED }
|
||||||
val position = layoutManager.findFirstVisibleItemPosition()
|
val position = layoutManager.findFirstVisibleItemPosition()
|
||||||
adapter.update(result.values)
|
adapter.update(result.values)
|
||||||
|
|
|
@ -9,20 +9,20 @@ import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
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.getAppName
|
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 com.stevesoltys.seedvault.restore.RestoreProgressAdapter.PackageViewHolder
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
||||||
|
|
||||||
private val items = LinkedList<AppRestoreResult>().apply {
|
private val items = LinkedList<AppRestoreResult>()
|
||||||
add(AppRestoreResult(MAGIC_PACKAGE_MANAGER, true))
|
|
||||||
}
|
|
||||||
private var isComplete = false
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PackageViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PackageViewHolder {
|
||||||
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item_app_status, parent, false)
|
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 getItemCount() = items.size
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: PackageViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: PackageViewHolder, position: Int) {
|
||||||
holder.bind(items[position], position == 0)
|
holder.bind(items[position])
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLatest(): AppRestoreResult {
|
fun update(newItems: LinkedList<AppRestoreResult>) {
|
||||||
return items[0]
|
val diffResult = DiffUtil.calculateDiff(Diff(items, newItems))
|
||||||
|
items.clear()
|
||||||
|
items.addAll(newItems)
|
||||||
|
diffResult.dispatchUpdatesTo(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLatestFailed() {
|
private class Diff(
|
||||||
items[0] = AppRestoreResult(items[0].packageName, false)
|
private val oldItems: LinkedList<AppRestoreResult>,
|
||||||
notifyItemChanged(0, items[0])
|
private val newItems: LinkedList<AppRestoreResult>) : DiffUtil.Callback() {
|
||||||
}
|
|
||||||
|
|
||||||
fun add(item: AppRestoreResult) {
|
override fun getOldListSize() = oldItems.size
|
||||||
items.addFirst(item)
|
override fun getNewListSize() = newItems.size
|
||||||
notifyItemInserted(0)
|
|
||||||
notifyItemRangeChanged(1, items.size - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setComplete(): List<String> {
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
isComplete = true
|
return oldItems[oldItemPosition].packageName == newItems[newItemPosition].packageName
|
||||||
notifyItemChanged(0)
|
}
|
||||||
return items.map { it.packageName }
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
return oldItems[oldItemPosition] == newItems[newItemPosition]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class PackageViewHolder(v: View) : ViewHolder(v) {
|
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 appStatus: ImageView = v.findViewById(R.id.appStatus)
|
||||||
private val progressBar: ProgressBar = v.findViewById(R.id.progressBar)
|
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) {
|
if (item.packageName == MAGIC_PACKAGE_MANAGER) {
|
||||||
appIcon.setImageResource(R.drawable.ic_launcher_default)
|
appIcon.setImageResource(R.drawable.ic_launcher_default)
|
||||||
appName.text = context.getString(R.string.restore_magic_package)
|
appName.text = context.getString(R.string.restore_magic_package)
|
||||||
|
@ -77,11 +79,14 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
||||||
}
|
}
|
||||||
appName.text = getAppName(pm, item.packageName)
|
appName.text = getAppName(pm, item.packageName)
|
||||||
}
|
}
|
||||||
if (isLatest && !isComplete) {
|
if (item.status == IN_PROGRESS) {
|
||||||
appStatus.visibility = INVISIBLE
|
appStatus.visibility = INVISIBLE
|
||||||
progressBar.visibility = VISIBLE
|
progressBar.visibility = VISIBLE
|
||||||
} else {
|
} 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
|
appStatus.visibility = VISIBLE
|
||||||
progressBar.visibility = INVISIBLE
|
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)
|
||||||
|
|
|
@ -51,28 +51,11 @@ class RestoreProgressFragment : Fragment() {
|
||||||
backupNameView.text = restorableBackup.name
|
backupNameView.text = restorableBackup.name
|
||||||
})
|
})
|
||||||
|
|
||||||
viewModel.restoreProgress.observe(this, Observer { currentPackage ->
|
viewModel.restoreProgress.observe(this, Observer { list ->
|
||||||
stayScrolledAtTop {
|
stayScrolledAtTop { adapter.update(list) }
|
||||||
val latest = adapter.getLatest()
|
|
||||||
if (viewModel.isFailedPackage(latest.packageName)) {
|
|
||||||
adapter.setLatestFailed()
|
|
||||||
}
|
|
||||||
adapter.add(AppRestoreResult(currentPackage, true))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
viewModel.restoreBackupResult.observe(this, Observer { finished ->
|
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
|
button.isEnabled = true
|
||||||
if (finished.hasError()) {
|
if (finished.hasError()) {
|
||||||
backupNameView.text = finished.errorMsg
|
backupNameView.text = finished.errorMsg
|
||||||
|
|
|
@ -15,8 +15,10 @@ import androidx.lifecycle.Transformations.switchMap
|
||||||
import androidx.lifecycle.asLiveData
|
import androidx.lifecycle.asLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.stevesoltys.seedvault.BackupMonitor
|
import com.stevesoltys.seedvault.BackupMonitor
|
||||||
|
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.restore.AppRestoreStatus.*
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
|
@ -35,6 +37,7 @@ import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.*
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
@ -73,8 +76,12 @@ internal class RestoreViewModel(
|
||||||
private val mNextButtonEnabled = MutableLiveData<Boolean>().apply { value = false }
|
private val mNextButtonEnabled = MutableLiveData<Boolean>().apply { value = false }
|
||||||
internal val nextButtonEnabled: LiveData<Boolean> = mNextButtonEnabled
|
internal val nextButtonEnabled: LiveData<Boolean> = mNextButtonEnabled
|
||||||
|
|
||||||
private val mRestoreProgress = MutableLiveData<String>()
|
private val mRestoreProgress = MutableLiveData<LinkedList<AppRestoreResult>>().apply {
|
||||||
internal val restoreProgress: LiveData<String> get() = mRestoreProgress
|
value = LinkedList<AppRestoreResult>().apply {
|
||||||
|
add(AppRestoreResult(MAGIC_PACKAGE_MANAGER, IN_PROGRESS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
internal val restoreProgress: LiveData<LinkedList<AppRestoreResult>> get() = mRestoreProgress
|
||||||
|
|
||||||
private val mRestoreBackupResult = MutableLiveData<RestoreBackupResult>()
|
private val mRestoreBackupResult = MutableLiveData<RestoreBackupResult>()
|
||||||
internal val restoreBackupResult: LiveData<RestoreBackupResult> get() = mRestoreBackupResult
|
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() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
|
@ -189,7 +235,7 @@ internal class RestoreViewModel(
|
||||||
* the current device. If no applicable datasets exist, restoreSets will be null.
|
* the current device. If no applicable datasets exist, restoreSets will be null.
|
||||||
*/
|
*/
|
||||||
override fun restoreSetsAvailable(restoreSets: Array<out RestoreSet>?) {
|
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()) {
|
val result = if (restoreSets == null || restoreSets.isEmpty()) {
|
||||||
RestoreSetResult(app.getString(R.string.restore_set_empty_result))
|
RestoreSetResult(app.getString(R.string.restore_set_empty_result))
|
||||||
|
@ -241,7 +287,7 @@ internal class RestoreViewModel(
|
||||||
*/
|
*/
|
||||||
override fun onUpdate(nowBeingRestored: Int, currentPackage: String) {
|
override fun onUpdate(nowBeingRestored: Int, currentPackage: String) {
|
||||||
// nowBeingRestored reporting is buggy, so don't use it
|
// 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
|
if (result == 0) null
|
||||||
else app.getString(R.string.restore_finished_error)
|
else app.getString(R.string.restore_finished_error)
|
||||||
)
|
)
|
||||||
mRestoreBackupResult.postValue(restoreResult)
|
onRestoreComplete(restoreResult)
|
||||||
closeSession()
|
closeSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue