Improve app data restore process
Apps are now restored alphabetically to be consistent with the other lists. Some irrelevant apps are hidden. Under the hood, we now use an AsyncListDiffer like in the other lists.
This commit is contained in:
parent
b3f93adf77
commit
fa19261d8e
5 changed files with 114 additions and 67 deletions
|
@ -201,6 +201,7 @@ open class App : Application() {
|
|||
|
||||
const val MAGIC_PACKAGE_MANAGER: String = PACKAGE_MANAGER_SENTINEL
|
||||
const val ANCESTRAL_RECORD_KEY = "@ancestral_record@"
|
||||
const val NO_DATA_END_SENTINEL = "@end@"
|
||||
const val GLOBAL_METADATA_KEY = "@meta@"
|
||||
const val ERROR_BACKUP_CANCELLED: Int = BackupManager.ERROR_BACKUP_CANCELLED
|
||||
|
||||
|
|
|
@ -20,7 +20,9 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import com.stevesoltys.seedvault.BackupMonitor
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.NO_DATA_END_SENTINEL
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||
import com.stevesoltys.seedvault.metadata.PackageState
|
||||
import com.stevesoltys.seedvault.restore.install.isInstalled
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
|
@ -37,9 +39,16 @@ import com.stevesoltys.seedvault.ui.AppBackupState.NOT_YET_BACKED_UP
|
|||
import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED
|
||||
import com.stevesoltys.seedvault.ui.notification.getAppName
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
|
||||
private val TAG = AppDataRestoreManager::class.simpleName
|
||||
|
||||
internal data class AppRestoreResult(
|
||||
val packageName: String,
|
||||
val name: String,
|
||||
val state: AppBackupState,
|
||||
)
|
||||
|
||||
internal class AppDataRestoreManager(
|
||||
private val context: Context,
|
||||
private val backupManager: IBackupManager,
|
||||
|
@ -50,17 +59,17 @@ internal class AppDataRestoreManager(
|
|||
private var session: IRestoreSession? = null
|
||||
private val monitor = BackupMonitor()
|
||||
|
||||
private val mRestoreProgress = MutableLiveData<LinkedList<AppRestoreResult>>().apply {
|
||||
value = LinkedList<AppRestoreResult>().apply {
|
||||
private val mRestoreProgress = MutableLiveData(
|
||||
LinkedList<AppRestoreResult>().apply {
|
||||
add(
|
||||
AppRestoreResult(
|
||||
packageName = MAGIC_PACKAGE_MANAGER,
|
||||
name = getAppName(context, MAGIC_PACKAGE_MANAGER),
|
||||
state = IN_PROGRESS
|
||||
name = getAppName(context, MAGIC_PACKAGE_MANAGER).toString(),
|
||||
state = IN_PROGRESS,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
val restoreProgress: LiveData<LinkedList<AppRestoreResult>> get() = mRestoreProgress
|
||||
private val mRestoreBackupResult = MutableLiveData<RestoreBackupResult>()
|
||||
val restoreBackupResult: LiveData<RestoreBackupResult> get() = mRestoreBackupResult
|
||||
|
@ -88,13 +97,13 @@ internal class AppDataRestoreManager(
|
|||
return
|
||||
}
|
||||
|
||||
val packages = restorableBackup.packageMetadataMap.keys.toList()
|
||||
val observer = RestoreObserver(
|
||||
restoreCoordinator = restoreCoordinator,
|
||||
restorableBackup = restorableBackup,
|
||||
session = session,
|
||||
packages = packages,
|
||||
monitor = monitor
|
||||
// sort packages (reverse) alphabetically, since we move from bottom to top
|
||||
packages = restorableBackup.packageMetadataMap.packagesSortedByNameDescending,
|
||||
monitor = monitor,
|
||||
)
|
||||
|
||||
// We need to retrieve the restore sets before starting the restore.
|
||||
|
@ -128,9 +137,12 @@ internal class AppDataRestoreManager(
|
|||
updateLatestPackage(list, backup)
|
||||
|
||||
// add current package
|
||||
list.addFirst(
|
||||
AppRestoreResult(packageName, getAppName(context, packageName), IN_PROGRESS)
|
||||
val name = getAppName(
|
||||
context = context,
|
||||
packageName = packageName,
|
||||
fallback = backup.packageMetadataMap[packageName]?.name?.toString() ?: packageName,
|
||||
)
|
||||
list.addFirst(AppRestoreResult(packageName, name.toString(), IN_PROGRESS))
|
||||
mRestoreProgress.postValue(list)
|
||||
}
|
||||
|
||||
|
@ -167,14 +179,25 @@ internal class AppDataRestoreManager(
|
|||
|
||||
// add missing packages as failed
|
||||
val seenPackages = list.map { it.packageName }.toSet()
|
||||
val expectedPackages = backup.packageMetadataMap.keys
|
||||
val expectedPackages =
|
||||
backup.packageMetadataMap.packagesSortedByNameDescending.toMutableSet()
|
||||
expectedPackages.removeAll(seenPackages)
|
||||
for (packageName: String in expectedPackages) {
|
||||
// TODO don't add if it was a NO_DATA system app
|
||||
for (packageName in expectedPackages) {
|
||||
val failedStatus = getFailedStatus(packageName, backup)
|
||||
val appResult =
|
||||
AppRestoreResult(packageName, getAppName(context, packageName), failedStatus)
|
||||
list.addFirst(appResult)
|
||||
if (failedStatus == FAILED_NO_DATA &&
|
||||
backup.packageMetadataMap[packageName]?.isInternalSystem == true
|
||||
) {
|
||||
// don't add internal system apps that had NO_DATA to backup
|
||||
} else {
|
||||
val name = getAppName(
|
||||
context = context,
|
||||
packageName = packageName,
|
||||
fallback = backup.packageMetadataMap[packageName]?.name?.toString()
|
||||
?: packageName,
|
||||
)
|
||||
val appResult = AppRestoreResult(packageName, name.toString(), failedStatus)
|
||||
list.addFirst(appResult)
|
||||
}
|
||||
}
|
||||
mRestoreProgress.postValue(list)
|
||||
|
||||
|
@ -186,7 +209,6 @@ internal class AppDataRestoreManager(
|
|||
session = null
|
||||
}
|
||||
|
||||
// TODO sort apps alphabetically
|
||||
@WorkerThread
|
||||
private inner class RestoreObserver(
|
||||
private val restoreCoordinator: RestoreCoordinator,
|
||||
|
@ -244,6 +266,7 @@ internal class AppDataRestoreManager(
|
|||
val token = backupMetadata.token
|
||||
val result = session.restorePackages(token, this, packageChunk, monitor)
|
||||
|
||||
@Suppress("UNRESOLVED_REFERENCE") // BackupManager.SUCCESS
|
||||
if (result != BackupManager.SUCCESS) {
|
||||
Log.e(TAG, "restorePackages() returned non-zero value: $result")
|
||||
}
|
||||
|
@ -295,6 +318,7 @@ internal class AppDataRestoreManager(
|
|||
}
|
||||
|
||||
private fun getRestoreResult(): RestoreBackupResult {
|
||||
@Suppress("UNRESOLVED_REFERENCE") // BackupManager.SUCCESS
|
||||
val failedChunks = chunkResults
|
||||
.filter { it.value != BackupManager.SUCCESS }
|
||||
.map { "chunk ${it.key} failed with error ${it.value}" }
|
||||
|
@ -310,4 +334,15 @@ internal class AppDataRestoreManager(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val PackageMetadataMap.packagesSortedByNameDescending: List<String>
|
||||
get() {
|
||||
return asIterable().sortedByDescending { (packageName, metadata) ->
|
||||
// sort packages (reverse) alphabetically, since we move from bottom to top
|
||||
(metadata.name?.toString() ?: packageName).lowercase(Locale.getDefault())
|
||||
}.mapNotNull {
|
||||
// don't try to restore this helper package, as it doesn't really exist
|
||||
if (it.key == NO_DATA_END_SENTINEL) null else it.key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,21 +6,40 @@
|
|||
package com.stevesoltys.seedvault.restore
|
||||
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil.ItemCallback
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.restore.RestoreProgressAdapter.PackageViewHolder
|
||||
import com.stevesoltys.seedvault.ui.AppBackupState
|
||||
import com.stevesoltys.seedvault.ui.AppViewHolder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.LinkedList
|
||||
|
||||
internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
||||
internal class RestoreProgressAdapter(
|
||||
val scope: CoroutineScope,
|
||||
val iconLoader: suspend (AppRestoreResult, (Drawable) -> Unit) -> Unit,
|
||||
) : Adapter<PackageViewHolder>() {
|
||||
|
||||
private val items = LinkedList<AppRestoreResult>()
|
||||
private val diffCallback = object : ItemCallback<AppRestoreResult>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: AppRestoreResult,
|
||||
newItem: AppRestoreResult,
|
||||
): Boolean {
|
||||
return oldItem.packageName == newItem.packageName
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(old: AppRestoreResult, new: AppRestoreResult): Boolean {
|
||||
return old.name == new.name && old.state == new.state
|
||||
}
|
||||
}
|
||||
private val differ = AsyncListDiffer(this, diffCallback)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PackageViewHolder {
|
||||
val v = LayoutInflater.from(parent.context)
|
||||
|
@ -28,37 +47,24 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
|||
return PackageViewHolder(v)
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
override fun getItemCount() = differ.currentList.size
|
||||
|
||||
override fun onBindViewHolder(holder: PackageViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
holder.bind(differ.currentList[position])
|
||||
}
|
||||
|
||||
fun update(newItems: LinkedList<AppRestoreResult>) {
|
||||
val diffResult = DiffUtil.calculateDiff(Diff(items, newItems))
|
||||
items.clear()
|
||||
items.addAll(newItems)
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
fun update(newItems: LinkedList<AppRestoreResult>, callback: Runnable) {
|
||||
// add .toList(), because [AppDataRestoreManager] still re-uses the same list,
|
||||
// but AsyncListDiffer needs a new one.
|
||||
differ.submitList(newItems.toList(), callback)
|
||||
}
|
||||
|
||||
private class Diff(
|
||||
private val oldItems: LinkedList<AppRestoreResult>,
|
||||
private val newItems: LinkedList<AppRestoreResult>,
|
||||
) : DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize() = oldItems.size
|
||||
override fun getNewListSize() = newItems.size
|
||||
|
||||
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]
|
||||
}
|
||||
override fun onViewRecycled(holder: PackageViewHolder) {
|
||||
holder.iconJob?.cancel()
|
||||
}
|
||||
|
||||
class PackageViewHolder(v: View) : AppViewHolder(v) {
|
||||
inner class PackageViewHolder(v: View) : AppViewHolder(v) {
|
||||
var iconJob: Job? = null
|
||||
fun bind(item: AppRestoreResult) {
|
||||
appName.text = item.name
|
||||
if (item.packageName == MAGIC_PACKAGE_MANAGER) {
|
||||
|
@ -67,7 +73,11 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
|||
try {
|
||||
appIcon.setImageDrawable(pm.getApplicationIcon(item.packageName))
|
||||
} catch (e: NameNotFoundException) {
|
||||
appIcon.setImageResource(R.drawable.ic_launcher_default)
|
||||
iconJob = scope.launch {
|
||||
iconLoader(item) { bitmap ->
|
||||
appIcon.setImageDrawable(bitmap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setState(item.state, true)
|
||||
|
@ -75,9 +85,3 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
internal data class AppRestoreResult(
|
||||
val packageName: String,
|
||||
val name: CharSequence,
|
||||
val state: AppBackupState,
|
||||
)
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package com.stevesoltys.seedvault.restore
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -16,6 +17,7 @@ import android.widget.TextView
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat.getColor
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.stevesoltys.seedvault.R
|
||||
|
@ -27,7 +29,7 @@ class RestoreProgressFragment : Fragment() {
|
|||
private val viewModel: RestoreViewModel by sharedViewModel()
|
||||
|
||||
private val layoutManager = LinearLayoutManager(context)
|
||||
private val adapter = RestoreProgressAdapter()
|
||||
private val adapter = RestoreProgressAdapter(lifecycleScope, this::loadIcon)
|
||||
|
||||
private lateinit var progressBar: ProgressBar
|
||||
private lateinit var titleView: TextView
|
||||
|
@ -67,17 +69,20 @@ class RestoreProgressFragment : Fragment() {
|
|||
// decryption will fail when the device is locked, so keep the screen on to prevent locking
|
||||
requireActivity().window.addFlags(FLAG_KEEP_SCREEN_ON)
|
||||
|
||||
viewModel.chosenRestorableBackup.observe(viewLifecycleOwner, { restorableBackup ->
|
||||
viewModel.chosenRestorableBackup.observe(viewLifecycleOwner) { restorableBackup ->
|
||||
backupNameView.text = restorableBackup.name
|
||||
progressBar.max = restorableBackup.packageMetadataMap.size
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.restoreProgress.observe(viewLifecycleOwner, { list ->
|
||||
stayScrolledAtTop { adapter.update(list) }
|
||||
viewModel.restoreProgress.observe(viewLifecycleOwner) { list ->
|
||||
progressBar.progress = list.size
|
||||
})
|
||||
val position = layoutManager.findFirstVisibleItemPosition()
|
||||
adapter.update(list) {
|
||||
if (position == 0) layoutManager.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.restoreBackupResult.observe(viewLifecycleOwner, { finished ->
|
||||
viewModel.restoreBackupResult.observe(viewLifecycleOwner) { finished ->
|
||||
button.isEnabled = true
|
||||
if (finished.hasError()) {
|
||||
backupNameView.text = finished.errorMsg
|
||||
|
@ -87,7 +92,7 @@ class RestoreProgressFragment : Fragment() {
|
|||
onRestoreFinished()
|
||||
}
|
||||
activity?.window?.clearFlags(FLAG_KEEP_SCREEN_ON)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRestoreFinished() {
|
||||
|
@ -103,10 +108,8 @@ class RestoreProgressFragment : Fragment() {
|
|||
.show()
|
||||
}
|
||||
|
||||
private fun stayScrolledAtTop(add: () -> Unit) {
|
||||
val position = layoutManager.findFirstVisibleItemPosition()
|
||||
add.invoke()
|
||||
if (position == 0) layoutManager.scrollToPosition(0)
|
||||
private suspend fun loadIcon(item: AppRestoreResult, callback: (Drawable) -> Unit) {
|
||||
viewModel.loadIcon(item.packageName, callback)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -167,14 +167,18 @@ internal class NotificationBackupObserver(
|
|||
|
||||
}
|
||||
|
||||
fun getAppName(context: Context, packageId: String): CharSequence {
|
||||
if (packageId == MAGIC_PACKAGE_MANAGER || packageId.startsWith("@")) {
|
||||
fun getAppName(
|
||||
context: Context,
|
||||
packageName: String,
|
||||
fallback: String = packageName,
|
||||
): CharSequence {
|
||||
if (packageName == MAGIC_PACKAGE_MANAGER || packageName.startsWith("@")) {
|
||||
return context.getString(R.string.restore_magic_package)
|
||||
}
|
||||
return try {
|
||||
val appInfo = context.packageManager.getApplicationInfo(packageId, 0)
|
||||
val appInfo = context.packageManager.getApplicationInfo(packageName, 0)
|
||||
context.packageManager.getApplicationLabel(appInfo)
|
||||
} catch (e: NameNotFoundException) {
|
||||
packageId
|
||||
fallback
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue