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 android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer 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.R
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.QUEUED
import com.stevesoltys.seedvault.transport.restore.InstallResult import com.stevesoltys.seedvault.transport.restore.InstallResult
import com.stevesoltys.seedvault.transport.restore.getInProgress import com.stevesoltys.seedvault.transport.restore.getInProgress
import kotlinx.android.synthetic.main.fragment_install_progress.* 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.backupNameView
import kotlinx.android.synthetic.main.fragment_restore_progress.currentPackageView
import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class InstallProgressFragment : Fragment() { class InstallProgressFragment : Fragment() {
private val viewModel: RestoreViewModel by sharedViewModel() private val viewModel: RestoreViewModel by sharedViewModel()
private val layoutManager = LinearLayoutManager(context)
private val adapter = InstallProgressAdapter()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? { savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_install_progress, container, false) 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?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
@ -33,16 +48,22 @@ class InstallProgressFragment : Fragment() {
viewModel.installResult.observe(this, Observer { result -> viewModel.installResult.observe(this, Observer { result ->
onInstallResult(result) onInstallResult(result)
}) })
viewModel.nextButtonEnabled.observe(this, Observer { enabled ->
nextButton.isEnabled = enabled
})
} }
private fun onInstallResult(installResult: InstallResult) { private fun onInstallResult(installResult: InstallResult) {
installResult.getInProgress()?.let { result -> val result = installResult.filterValues { it.status != QUEUED }
currentPackageView.text = result.name val position = layoutManager.findFirstVisibleItemPosition()
result.icon?.let { currentPackageImageView.setImageDrawable(it) } adapter.update(result.values)
progressBar.progress = result.progress if (position == 0) layoutManager.scrollToPosition(0)
progressBar.max = result.total
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) getInstallResult(backup)
} }
private val mNextButtonEnabled = MutableLiveData<Boolean>().apply { value = false }
internal val nextButtonEnabled: LiveData<Boolean> = mNextButtonEnabled
private val mRestoreProgress = MutableLiveData<String>() private val mRestoreProgress = MutableLiveData<String>()
internal val restoreProgress: LiveData<String> get() = mRestoreProgress internal val restoreProgress: LiveData<String> get() = mRestoreProgress
@ -125,13 +128,20 @@ internal class RestoreViewModel(
Log.d(TAG, "Exception in InstallResult Flow", e) Log.d(TAG, "Exception in InstallResult Flow", e)
}.onCompletion { e -> }.onCompletion { e ->
Log.d(TAG, "Completed InstallResult Flow", e) Log.d(TAG, "Completed InstallResult Flow", e)
mDisplayFragment.postEvent(RESTORE_BACKUP) mNextButtonEnabled.postValue(true)
startRestore(restorableBackup.token)
} }
.flowOn(ioDispatcher) .flowOn(ioDispatcher)
.asLiveData() .asLiveData()
} }
internal fun onNextClicked() {
mDisplayFragment.postEvent(RESTORE_BACKUP)
val token = mChosenRestorableBackup.value?.token ?: throw AssertionError()
viewModelScope.launch(ioDispatcher) {
startRestore(token)
}
}
@WorkerThread @WorkerThread
private suspend fun startRestore(token: Long) { private suspend fun startRestore(token: Long) {
Log.d(TAG, "Starting new restore session to restore backup $token") Log.d(TAG, "Starting new restore session to restore backup $token")

View file

@ -40,7 +40,7 @@ internal class ApkRestore(
val installResult = MutableInstallResult(total) val installResult = MutableInstallResult(total)
packages.forEach { (packageName, _) -> packages.forEach { (packageName, _) ->
progress++ progress++
installResult[packageName] = ApkRestoreResult(progress, total, QUEUED) installResult[packageName] = ApkRestoreResult(packageName, progress, total, QUEUED)
} }
emit(installResult) emit(installResult)
@ -155,12 +155,17 @@ internal class MutableInstallResult(initialCapacity: Int) : ConcurrentHashMap<St
} }
internal data class ApkRestoreResult( internal data class ApkRestoreResult(
val packageName: CharSequence,
val progress: Int, val progress: Int,
val total: Int, val total: Int,
val status: ApkRestoreStatus, val status: ApkRestoreStatus,
val name: CharSequence? = null, val name: CharSequence? = null,
val icon: Drawable? = null val icon: Drawable? = null
) ) : Comparable<ApkRestoreResult> {
override fun compareTo(other: ApkRestoreResult): Int {
return other.progress.compareTo(progress)
}
}
internal enum class ApkRestoreStatus { internal enum class ApkRestoreStatus {
QUEUED, IN_PROGRESS, SUCCEEDED, FAILED 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" app:layout_constraintTop_toBottomOf="@+id/titleView"
tools:text="Pixel 2 XL" /> tools:text="Pixel 2 XL" />
<ImageView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/currentPackageImageView" android:id="@+id/appList"
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"
android:layout_width="0dp" 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_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:gravity="center_horizontal" android:layout_marginBottom="16dp"
android:textColor="?android:textColorSecondary" android:enabled="false"
android:text="@string/restore_next"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/currentPackageImageView" app:layout_constraintHorizontal_bias="1.0"
tools:text="@string/restore_current_package" /> app:layout_constraintStart_toStartOf="parent" />
<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" />
</androidx.constraintlayout.widget.ConstraintLayout> </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> <resources>
<color name="accent">#99cc00</color> <color name="accent">#99cc00</color>
<color name="divider">#8A000000</color> <color name="divider">#8A000000</color>
<color name="green">#558B2F</color>
<color name="red">#D32F2F</color> <color name="red">#D32F2F</color>
</resources> </resources>

View file

@ -89,6 +89,7 @@
<string name="restore_set_error">An error occurred while loading the backups.</string> <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_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_installing_packages">Re-installing Apps</string>
<string name="restore_next">Next</string>
<string name="restore_restoring">Restoring Backup</string> <string name="restore_restoring">Restoring Backup</string>
<string name="restore_current_package">Restoring %s…</string> <string name="restore_current_package">Restoring %s…</string>
<string name="restore_finished_success">Restore complete.</string> <string name="restore_finished_success">Restore complete.</string>