From fa19261d8eb95934f65fedd51fb7ab9d1c2e8373 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 31 May 2024 16:48:27 -0300 Subject: [PATCH] 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. --- .../java/com/stevesoltys/seedvault/App.kt | 1 + .../restore/AppDataRestoreManager.kt | 69 +++++++++++++----- .../restore/RestoreProgressAdapter.kt | 72 ++++++++++--------- .../restore/RestoreProgressFragment.kt | 27 +++---- .../NotificationBackupObserver.kt | 12 ++-- 5 files changed, 114 insertions(+), 67 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index 8daa8eac..4391067f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -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 diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/AppDataRestoreManager.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/AppDataRestoreManager.kt index de840750..88caefc0 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/AppDataRestoreManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/AppDataRestoreManager.kt @@ -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>().apply { - value = LinkedList().apply { + private val mRestoreProgress = MutableLiveData( + LinkedList().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> get() = mRestoreProgress private val mRestoreBackupResult = MutableLiveData() val restoreBackupResult: LiveData 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 + 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 + } + } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressAdapter.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressAdapter.kt index ae235703..fff602d3 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressAdapter.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressAdapter.kt @@ -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() { +internal class RestoreProgressAdapter( + val scope: CoroutineScope, + val iconLoader: suspend (AppRestoreResult, (Drawable) -> Unit) -> Unit, +) : Adapter() { - private val items = LinkedList() + private val diffCallback = object : ItemCallback() { + 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() { 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) { - val diffResult = DiffUtil.calculateDiff(Diff(items, newItems)) - items.clear() - items.addAll(newItems) - diffResult.dispatchUpdatesTo(this) + fun update(newItems: LinkedList, 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, - private val newItems: LinkedList, - ) : 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() { 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() { } } - -internal data class AppRestoreResult( - val packageName: String, - val name: CharSequence, - val state: AppBackupState, -) diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressFragment.kt index d6085843..21b2f228 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressFragment.kt @@ -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) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt index 2bbb08dc..d45dca91 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt @@ -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 } }