Show list of re-installed apps and let the user review it before restoring data

This commit is contained in:
Torsten Grote 2020-01-08 15:26:58 -03:00
parent debaca0e2c
commit 96a4642f4f
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
10 changed files with 212 additions and 41 deletions

View file

@ -0,0 +1,72 @@
package com.stevesoltys.seedvault.restore
import android.view.LayoutInflater
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.transport.restore.ApkRestoreResult
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.*
internal class InstallProgressAdapter : Adapter<AppViewHolder>() {
private val items = SortedList<ApkRestoreResult>(ApkRestoreResult::class.java, object : SortedListAdapterCallback<ApkRestoreResult>(this) {
override fun areItemsTheSame(item1: ApkRestoreResult, item2: ApkRestoreResult) = item1.packageName == item2.packageName
override fun areContentsTheSame(oldItem: ApkRestoreResult, newItem: ApkRestoreResult) = oldItem == newItem
override fun compare(item1: ApkRestoreResult, item2: ApkRestoreResult) = item1.compareTo(item2)
})
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item_app_status, parent, false)
return AppViewHolder(v)
}
override fun getItemCount() = items.size()
override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
holder.bind(items[position])
}
fun update(items: Collection<ApkRestoreResult>) {
this.items.replaceAll(items)
}
}
internal class AppViewHolder(v: View) : ViewHolder(v) {
private val appIcon: ImageView = v.findViewById(R.id.appIcon)
private val appName: TextView = v.findViewById(R.id.appName)
private val appStatus: ImageView = v.findViewById(R.id.appStatus)
private val progressBar: ProgressBar = v.findViewById(R.id.progressBar)
fun bind(item: ApkRestoreResult) {
appIcon.setImageDrawable(item.icon)
appName.text = item.name
when (item.status) {
IN_PROGRESS -> {
appStatus.visibility = INVISIBLE
progressBar.visibility = VISIBLE
}
SUCCEEDED -> {
appStatus.setImageResource(R.drawable.ic_check_green)
appStatus.visibility = VISIBLE
progressBar.visibility = INVISIBLE
}
FAILED -> {
appStatus.setImageResource(R.drawable.ic_cancel_red)
appStatus.visibility = VISIBLE
progressBar.visibility = INVISIBLE
}
QUEUED -> throw AssertionError()
}
}
}

View file

@ -6,23 +6,38 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.QUEUED
import com.stevesoltys.seedvault.transport.restore.InstallResult
import com.stevesoltys.seedvault.transport.restore.getInProgress
import kotlinx.android.synthetic.main.fragment_install_progress.*
import kotlinx.android.synthetic.main.fragment_restore_progress.backupNameView
import kotlinx.android.synthetic.main.fragment_restore_progress.currentPackageView
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class InstallProgressFragment : Fragment() {
private val viewModel: RestoreViewModel by sharedViewModel()
private val layoutManager = LinearLayoutManager(context)
private val adapter = InstallProgressAdapter()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_install_progress, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
appList.apply {
layoutManager = this@InstallProgressFragment.layoutManager
adapter = this@InstallProgressFragment.adapter
addItemDecoration(DividerItemDecoration(context, VERTICAL))
}
nextButton.setOnClickListener { viewModel.onNextClicked() }
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
@ -33,16 +48,22 @@ class InstallProgressFragment : Fragment() {
viewModel.installResult.observe(this, Observer { result ->
onInstallResult(result)
})
viewModel.nextButtonEnabled.observe(this, Observer { enabled ->
nextButton.isEnabled = enabled
})
}
private fun onInstallResult(installResult: InstallResult) {
installResult.getInProgress()?.let { result ->
currentPackageView.text = result.name
result.icon?.let { currentPackageImageView.setImageDrawable(it) }
progressBar.progress = result.progress
progressBar.max = result.total
val result = installResult.filterValues { it.status != QUEUED }
val position = layoutManager.findFirstVisibleItemPosition()
adapter.update(result.values)
if (position == 0) layoutManager.scrollToPosition(0)
result.getInProgress()?.let {
progressBar.progress = it.progress
progressBar.max = it.total
}
// TODO add finished apps to list of (failed?) apps and continue on button press
}
}

View file

@ -70,6 +70,9 @@ internal class RestoreViewModel(
getInstallResult(backup)
}
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
@ -125,13 +128,20 @@ internal class RestoreViewModel(
Log.d(TAG, "Exception in InstallResult Flow", e)
}.onCompletion { e ->
Log.d(TAG, "Completed InstallResult Flow", e)
mDisplayFragment.postEvent(RESTORE_BACKUP)
startRestore(restorableBackup.token)
mNextButtonEnabled.postValue(true)
}
.flowOn(ioDispatcher)
.asLiveData()
}
internal fun onNextClicked() {
mDisplayFragment.postEvent(RESTORE_BACKUP)
val token = mChosenRestorableBackup.value?.token ?: throw AssertionError()
viewModelScope.launch(ioDispatcher) {
startRestore(token)
}
}
@WorkerThread
private suspend fun startRestore(token: Long) {
Log.d(TAG, "Starting new restore session to restore backup $token")

View file

@ -40,7 +40,7 @@ internal class ApkRestore(
val installResult = MutableInstallResult(total)
packages.forEach { (packageName, _) ->
progress++
installResult[packageName] = ApkRestoreResult(progress, total, QUEUED)
installResult[packageName] = ApkRestoreResult(packageName, progress, total, QUEUED)
}
emit(installResult)
@ -155,12 +155,17 @@ internal class MutableInstallResult(initialCapacity: Int) : ConcurrentHashMap<St
}
internal data class ApkRestoreResult(
val packageName: CharSequence,
val progress: Int,
val total: Int,
val status: ApkRestoreStatus,
val name: CharSequence? = null,
val icon: Drawable? = null
)
) : Comparable<ApkRestoreResult> {
override fun compareTo(other: ApkRestoreResult): Int {
return other.progress.compareTo(progress)
}
}
internal enum class ApkRestoreStatus {
QUEUED, IN_PROGRESS, SUCCEEDED, FAILED

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/red"
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/green"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
</vector>

View file

@ -54,40 +54,30 @@
app:layout_constraintTop_toBottomOf="@+id/titleView"
tools:text="Pixel 2 XL" />
<ImageView
android:id="@+id/currentPackageImageView"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginTop="16dp"
android:scaleType="fitCenter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/backupNameView"
tools:ignore="ContentDescription"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/currentPackageView"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/appList"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="16dp"
app:layout_constraintBottom_toTopOf="@+id/nextButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/backupNameView"
app:layout_constraintStart_toStartOf="parent"
tools:listitem="@layout/list_item_app_status" />
<Button
android:id="@+id/nextButton"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:gravity="center_horizontal"
android:textColor="?android:textColorSecondary"
android:layout_marginBottom="16dp"
android:enabled="false"
android:text="@string/restore_next"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/currentPackageImageView"
tools:text="@string/restore_current_package" />
<ProgressBar
android:id="@+id/roundProgressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/currentPackageView" />
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp">
<ImageView
android:id="@+id/appIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/appName"
style="@style/TextAppearance.AppCompat.Medium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/appStatus"
app:layout_constraintStart_toEndOf="@+id/appIcon"
app:layout_constraintTop_toTopOf="parent"
tools:text="Seedvault Backup" />
<ImageView
android:id="@+id/appStatus"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:srcCompat="@tools:sample/avatars" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -2,5 +2,6 @@
<resources>
<color name="accent">#99cc00</color>
<color name="divider">#8A000000</color>
<color name="green">#558B2F</color>
<color name="red">#D32F2F</color>
</resources>

View file

@ -89,6 +89,7 @@
<string name="restore_set_error">An error occurred while loading the backups.</string>
<string name="restore_set_empty_result">No suitable backups found at given location.\n\nThis is most likely due to a wrong recovery code or a storage error.</string>
<string name="restore_installing_packages">Re-installing Apps</string>
<string name="restore_next">Next</string>
<string name="restore_restoring">Restoring Backup</string>
<string name="restore_current_package">Restoring %s…</string>
<string name="restore_finished_success">Restore complete.</string>