Skip to content

Commit

Permalink
Improve app data restore process
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
grote committed May 31, 2024
1 parent b3f93ad commit 4fc9923
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 67 deletions.
1 change: 1 addition & 0 deletions app/src/main/java/com/stevesoltys/seedvault/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -167,14 +179,27 @@ 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 (packageName == NO_DATA_END_SENTINEL) {
// don't show this helper package in the list, as it doesn't really exist
} else 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)

Expand All @@ -186,7 +211,6 @@ internal class AppDataRestoreManager(
session = null
}

// TODO sort apps alphabetically
@WorkerThread
private inner class RestoreObserver(
private val restoreCoordinator: RestoreCoordinator,
Expand Down Expand Up @@ -244,6 +268,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")
}
Expand Down Expand Up @@ -295,6 +320,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}" }
Expand All @@ -310,4 +336,12 @@ 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())
}.map { it.key }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,59 +6,65 @@
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)
.inflate(R.layout.list_item_app_status, parent, false)
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) {
Expand All @@ -67,17 +73,15 @@ 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)
}
}

}

internal data class AppRestoreResult(
val packageName: String,
val name: CharSequence,
val state: AppBackupState,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -87,7 +92,7 @@ class RestoreProgressFragment : Fragment() {
onRestoreFinished()
}
activity?.window?.clearFlags(FLAG_KEEP_SCREEN_ON)
})
}
}

private fun onRestoreFinished() {
Expand All @@ -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)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

0 comments on commit 4fc9923

Please sign in to comment.