Show list of re-installed apps and let the user review it before restoring data
This commit is contained in:
parent
debaca0e2c
commit
96a4642f4f
10 changed files with 212 additions and 41 deletions
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
9
app/src/main/res/drawable/ic_cancel_red.xml
Normal file
9
app/src/main/res/drawable/ic_cancel_red.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_check_green.xml
Normal file
9
app/src/main/res/drawable/ic_check_green.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
53
app/src/main/res/layout/list_item_app_status.xml
Normal file
53
app/src/main/res/layout/list_item_app_status.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue