diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/InstallProgressFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/InstallProgressFragment.kt index 0914b45d..ad22c9a4 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/InstallProgressFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/InstallProgressFragment.kt @@ -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) diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressAdapter.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressAdapter.kt index 7bd09f1b..442025d7 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressAdapter.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressAdapter.kt @@ -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() { - private val items = LinkedList().apply { - add(AppRestoreResult(MAGIC_PACKAGE_MANAGER, true)) - } - private var isComplete = false + private val items = LinkedList() 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() { 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) { + 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, + private val newItems: LinkedList) : 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 { - 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() { 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() { } 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() { } -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) diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressFragment.kt index 8fc4d164..aed92479 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressFragment.kt @@ -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 diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt index b8b6dd1a..24543bce 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt @@ -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().apply { value = false } internal val nextButtonEnabled: LiveData = mNextButtonEnabled - private val mRestoreProgress = MutableLiveData() - internal val restoreProgress: LiveData get() = mRestoreProgress + private val mRestoreProgress = MutableLiveData>().apply { + value = LinkedList().apply { + add(AppRestoreResult(MAGIC_PACKAGE_MANAGER, IN_PROGRESS)) + } + } + internal val restoreProgress: LiveData> get() = mRestoreProgress private val mRestoreBackupResult = MutableLiveData() internal val restoreBackupResult: LiveData 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) { + 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?) { - 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() }