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:
Torsten Grote 2024-05-31 16:48:27 -03:00
parent b3f93adf77
commit fa19261d8e
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
5 changed files with 114 additions and 67 deletions

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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,
)

View file

@ -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)
}
}

View file

@ -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
}
}