Factor out code into new

AppDataRestoreManager

which was in RestoreViewModel before.
Now all three steps of app restore have their own dedicated manager class making the ViewModel more readable.
This commit is contained in:
Torsten Grote 2024-05-31 15:05:14 -03:00
parent 66f3852edf
commit b3f93adf77
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
2 changed files with 323 additions and 302 deletions

View file

@ -0,0 +1,313 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.restore
import android.app.backup.BackupManager
import android.app.backup.BackupTransport
import android.app.backup.IBackupManager
import android.app.backup.IRestoreObserver
import android.app.backup.IRestoreSession
import android.app.backup.RestoreSet
import android.content.Context
import android.os.RemoteException
import android.os.UserHandle
import android.util.Log
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.stevesoltys.seedvault.BackupMonitor
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.metadata.PackageState
import com.stevesoltys.seedvault.restore.install.isInstalled
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
import com.stevesoltys.seedvault.ui.AppBackupState
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_ALLOWED
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_INSTALLED
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NO_DATA
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_QUOTA_EXCEEDED
import com.stevesoltys.seedvault.ui.AppBackupState.IN_PROGRESS
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
private val TAG = AppDataRestoreManager::class.simpleName
internal class AppDataRestoreManager(
private val context: Context,
private val backupManager: IBackupManager,
private val settingsManager: SettingsManager,
private val restoreCoordinator: RestoreCoordinator,
) {
private var session: IRestoreSession? = null
private val monitor = BackupMonitor()
private val mRestoreProgress = MutableLiveData<LinkedList<AppRestoreResult>>().apply {
value = LinkedList<AppRestoreResult>().apply {
add(
AppRestoreResult(
packageName = MAGIC_PACKAGE_MANAGER,
name = getAppName(context, MAGIC_PACKAGE_MANAGER),
state = IN_PROGRESS
)
)
}
}
val restoreProgress: LiveData<LinkedList<AppRestoreResult>> get() = mRestoreProgress
private val mRestoreBackupResult = MutableLiveData<RestoreBackupResult>()
val restoreBackupResult: LiveData<RestoreBackupResult> get() = mRestoreBackupResult
@WorkerThread
fun startRestore(restorableBackup: RestorableBackup) {
val token = restorableBackup.token
Log.d(TAG, "Starting new restore session to restore backup $token")
// if we had no token before (i.e. restore from setup wizard),
// use the token of the current restore set from now on
if (settingsManager.getToken() == null) {
settingsManager.setNewToken(token)
}
// start a new restore session
val session = try {
getOrStartSession()
} catch (e: RemoteException) {
Log.e(TAG, "Error starting new session", e)
mRestoreBackupResult.postValue(
RestoreBackupResult(context.getString(R.string.restore_set_error))
)
return
}
val packages = restorableBackup.packageMetadataMap.keys.toList()
val observer = RestoreObserver(
restoreCoordinator = restoreCoordinator,
restorableBackup = restorableBackup,
session = session,
packages = packages,
monitor = monitor
)
// We need to retrieve the restore sets before starting the restore.
// Otherwise, restorePackages() won't work as they need the restore sets cached internally.
if (session.getAvailableRestoreSets(observer, monitor) != 0) {
Log.e(TAG, "getAvailableRestoreSets() returned non-zero value")
mRestoreBackupResult.postValue(
RestoreBackupResult(context.getString(R.string.restore_set_error))
)
}
}
@Throws(RemoteException::class)
private fun getOrStartSession(): IRestoreSession {
@Suppress("UNRESOLVED_REFERENCE")
val session = this.session
?: backupManager.beginRestoreSessionForUser(UserHandle.myUserId(), null, TRANSPORT_ID)
?: throw RemoteException("beginRestoreSessionForUser returned null")
this.session = session
return session
}
@WorkerThread
// this should be called one package at a time and never concurrently for different packages
private fun onRestoreStarted(packageName: String, backup: RestorableBackup) {
// list is never null and always has at least one package
val list = mRestoreProgress.value!!
// check previous package first and change status
updateLatestPackage(list, backup)
// add current package
list.addFirst(
AppRestoreResult(packageName, getAppName(context, packageName), IN_PROGRESS)
)
mRestoreProgress.postValue(list)
}
@WorkerThread
private fun updateLatestPackage(list: LinkedList<AppRestoreResult>, backup: RestorableBackup) {
val latestResult = list[0]
if (restoreCoordinator.isFailedPackage(latestResult.packageName)) {
list[0] = latestResult.copy(state = getFailedStatus(latestResult.packageName, backup))
} else {
list[0] = latestResult.copy(state = SUCCEEDED)
}
}
@WorkerThread
private fun getFailedStatus(packageName: String, backup: RestorableBackup): AppBackupState {
val metadata = backup.packageMetadataMap[packageName] ?: return FAILED
return when (metadata.state) {
PackageState.NO_DATA -> FAILED_NO_DATA
PackageState.WAS_STOPPED -> NOT_YET_BACKED_UP
PackageState.NOT_ALLOWED -> FAILED_NOT_ALLOWED
PackageState.QUOTA_EXCEEDED -> FAILED_QUOTA_EXCEEDED
PackageState.UNKNOWN_ERROR -> FAILED
PackageState.APK_AND_DATA -> if (context.packageManager.isInstalled(packageName)) {
FAILED
} else FAILED_NOT_INSTALLED
}
}
@WorkerThread
private fun onRestoreComplete(result: RestoreBackupResult, backup: RestorableBackup) {
// update status of latest package
val list = mRestoreProgress.value!!
updateLatestPackage(list, backup)
// add missing packages as failed
val seenPackages = list.map { it.packageName }.toSet()
val expectedPackages = backup.packageMetadataMap.keys
expectedPackages.removeAll(seenPackages)
for (packageName: String in expectedPackages) {
// TODO don't add if it was a NO_DATA system app
val failedStatus = getFailedStatus(packageName, backup)
val appResult =
AppRestoreResult(packageName, getAppName(context, packageName), failedStatus)
list.addFirst(appResult)
}
mRestoreProgress.postValue(list)
mRestoreBackupResult.postValue(result)
}
fun closeSession() {
session?.endRestoreSession()
session = null
}
// TODO sort apps alphabetically
@WorkerThread
private inner class RestoreObserver(
private val restoreCoordinator: RestoreCoordinator,
private val restorableBackup: RestorableBackup,
private val session: IRestoreSession,
private val packages: List<String>,
private val monitor: BackupMonitor,
) : IRestoreObserver.Stub() {
/**
* The current package index.
*
* Used for splitting the packages into chunks.
*/
private var packageIndex: Int = 0
/**
* Map of results for each chunk.
*
* The key is the chunk index, the value is the result.
*/
private val chunkResults = mutableMapOf<Int, Int>()
/**
* Supply a list of the restore datasets available from the current transport.
* This method is invoked as a callback following the application's use of the
* [IRestoreSession.getAvailableRestoreSets] method.
*
* @param restoreSets An array of [RestoreSet] objects
* describing all of the available datasets that are candidates for restoring to
* the current device. If no applicable datasets exist, restoreSets will be null.
*/
override fun restoreSetsAvailable(restoreSets: Array<out RestoreSet>?) {
// this gets executed after we got the restore sets
// now we can start the restore of all available packages
restoreNextPackages()
}
/**
* Restore the next chunk of packages.
*
* We need to restore in chunks, otherwise [BackupTransport.startRestore] in the
* framework's [PerformUnifiedRestoreTask] may fail due to an oversize Binder
* transaction, causing the entire restoration to fail.
*/
private fun restoreNextPackages() {
// Make sure metadata for selected backup is cached before starting each chunk.
val backupMetadata = restorableBackup.backupMetadata
restoreCoordinator.beforeStartRestore(backupMetadata)
val nextChunkIndex = (packageIndex + PACKAGES_PER_CHUNK).coerceAtMost(packages.size)
val packageChunk = packages.subList(packageIndex, nextChunkIndex).toTypedArray()
packageIndex += packageChunk.size
val token = backupMetadata.token
val result = session.restorePackages(token, this, packageChunk, monitor)
if (result != BackupManager.SUCCESS) {
Log.e(TAG, "restorePackages() returned non-zero value: $result")
}
}
/**
* The restore operation has begun.
*
* @param numPackages The total number of packages
* being processed in this restore operation.
*/
override fun restoreStarting(numPackages: Int) {
// noop
}
/**
* An indication of which package is being restored currently,
* out of the total number provided in the [restoreStarting] callback.
* This method is not guaranteed to be called.
*
* @param nowBeingRestored The index, between 1 and the numPackages parameter
* to the [restoreStarting] callback, of the package now being restored.
* @param currentPackage The name of the package now being restored.
*/
override fun onUpdate(nowBeingRestored: Int, currentPackage: String) {
// nowBeingRestored reporting is buggy, so don't use it
onRestoreStarted(currentPackage, restorableBackup)
}
/**
* The restore operation has completed.
*
* @param result Zero on success; a nonzero error code if the restore operation
* as a whole failed.
*/
override fun restoreFinished(result: Int) {
val chunkIndex = packageIndex / PACKAGES_PER_CHUNK
chunkResults[chunkIndex] = result
// Restore next chunk if successful and there are more packages to restore.
if (packageIndex < packages.size) {
restoreNextPackages()
return
}
// Restore finished, time to get the result.
onRestoreComplete(getRestoreResult(), restorableBackup)
closeSession()
}
private fun getRestoreResult(): RestoreBackupResult {
val failedChunks = chunkResults
.filter { it.value != BackupManager.SUCCESS }
.map { "chunk ${it.key} failed with error ${it.value}" }
return if (failedChunks.isNotEmpty()) {
Log.e(TAG, "Restore failed: $failedChunks")
return RestoreBackupResult(
errorMsg = context.getString(R.string.restore_finished_error)
)
} else {
RestoreBackupResult(errorMsg = null)
}
}
}
}

View file

@ -6,34 +6,18 @@
package com.stevesoltys.seedvault.restore package com.stevesoltys.seedvault.restore
import android.app.Application import android.app.Application
import android.app.backup.BackupManager
import android.app.backup.BackupTransport
import android.app.backup.IBackupManager import android.app.backup.IBackupManager
import android.app.backup.IRestoreObserver
import android.app.backup.IRestoreSession
import android.app.backup.RestoreSet
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.RemoteException
import android.os.UserHandle
import android.util.Log import android.util.Log
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.appcompat.content.res.AppCompatResources.getDrawable
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.stevesoltys.seedvault.BackupMonitor
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.plugins.StoragePluginManager import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
@ -43,25 +27,13 @@ import com.stevesoltys.seedvault.restore.DisplayFragment.SELECT_APPS
import com.stevesoltys.seedvault.restore.install.ApkRestore import com.stevesoltys.seedvault.restore.install.ApkRestore
import com.stevesoltys.seedvault.restore.install.InstallIntentCreator import com.stevesoltys.seedvault.restore.install.InstallIntentCreator
import com.stevesoltys.seedvault.restore.install.InstallResult import com.stevesoltys.seedvault.restore.install.InstallResult
import com.stevesoltys.seedvault.restore.install.isInstalled
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.storage.StorageRestoreService import com.stevesoltys.seedvault.storage.StorageRestoreService
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
import com.stevesoltys.seedvault.ui.AppBackupState
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_ALLOWED
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_INSTALLED
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NO_DATA
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_QUOTA_EXCEEDED
import com.stevesoltys.seedvault.ui.AppBackupState.IN_PROGRESS
import com.stevesoltys.seedvault.ui.AppBackupState.NOT_YET_BACKED_UP
import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED
import com.stevesoltys.seedvault.ui.LiveEvent import com.stevesoltys.seedvault.ui.LiveEvent
import com.stevesoltys.seedvault.ui.MutableLiveEvent import com.stevesoltys.seedvault.ui.MutableLiveEvent
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SYSTEM import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SYSTEM
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
import com.stevesoltys.seedvault.ui.notification.getAppName
import com.stevesoltys.seedvault.ui.systemData import com.stevesoltys.seedvault.ui.systemData
import com.stevesoltys.seedvault.worker.IconManager import com.stevesoltys.seedvault.worker.IconManager
import com.stevesoltys.seedvault.worker.NUM_PACKAGES_PER_TRANSACTION import com.stevesoltys.seedvault.worker.NUM_PACKAGES_PER_TRANSACTION
@ -85,7 +57,7 @@ internal class RestoreViewModel(
app: Application, app: Application,
settingsManager: SettingsManager, settingsManager: SettingsManager,
keyManager: KeyManager, keyManager: KeyManager,
private val backupManager: IBackupManager, backupManager: IBackupManager,
private val restoreCoordinator: RestoreCoordinator, private val restoreCoordinator: RestoreCoordinator,
private val apkRestore: ApkRestore, private val apkRestore: ApkRestore,
private val iconManager: IconManager, private val iconManager: IconManager,
@ -97,10 +69,10 @@ internal class RestoreViewModel(
override val isRestoreOperation = true override val isRestoreOperation = true
private var session: IRestoreSession? = null
private val monitor = BackupMonitor()
private val appSelectionManager = private val appSelectionManager =
AppSelectionManager(app, pluginManager, iconManager, viewModelScope) AppSelectionManager(app, pluginManager, iconManager, viewModelScope)
private val appDataRestoreManager =
AppDataRestoreManager(app, backupManager, settingsManager, restoreCoordinator)
private val mDisplayFragment = MutableLiveEvent<DisplayFragment>() private val mDisplayFragment = MutableLiveEvent<DisplayFragment>()
internal val displayFragment: LiveEvent<DisplayFragment> = mDisplayFragment internal val displayFragment: LiveEvent<DisplayFragment> = mDisplayFragment
@ -118,34 +90,14 @@ internal class RestoreViewModel(
internal val installIntentCreator by lazy { InstallIntentCreator(app.packageManager) } internal val installIntentCreator by lazy { InstallIntentCreator(app.packageManager) }
private val mRestoreProgress = MutableLiveData<LinkedList<AppRestoreResult>>().apply { internal val restoreProgress: LiveData<LinkedList<AppRestoreResult>>
value = LinkedList<AppRestoreResult>().apply { get() = appDataRestoreManager.restoreProgress
add(
AppRestoreResult(
packageName = MAGIC_PACKAGE_MANAGER,
name = getAppName(app, MAGIC_PACKAGE_MANAGER),
state = IN_PROGRESS
)
)
}
}
internal val restoreProgress: LiveData<LinkedList<AppRestoreResult>> get() = mRestoreProgress
private val mRestoreBackupResult = MutableLiveData<RestoreBackupResult>() internal val restoreBackupResult: LiveData<RestoreBackupResult>
internal val restoreBackupResult: LiveData<RestoreBackupResult> get() = mRestoreBackupResult get() = appDataRestoreManager.restoreBackupResult
override val snapshots = storageBackup.getBackupSnapshots().asLiveData(ioDispatcher) override val snapshots = storageBackup.getBackupSnapshots().asLiveData(ioDispatcher)
@Throws(RemoteException::class)
private fun getOrStartSession(): IRestoreSession {
@Suppress("UNRESOLVED_REFERENCE")
val session = this.session
?: backupManager.beginRestoreSessionForUser(UserHandle.myUserId(), null, TRANSPORT_ID)
?: throw RemoteException("beginRestoreSessionForUser returned null")
this.session = session
return session
}
internal fun loadRestoreSets() = viewModelScope.launch(ioDispatcher) { internal fun loadRestoreSets() = viewModelScope.launch(ioDispatcher) {
val backups = restoreCoordinator.getAvailableMetadata()?.mapNotNull { (token, metadata) -> val backups = restoreCoordinator.getAvailableMetadata()?.mapNotNull { (token, metadata) ->
when (metadata.time) { when (metadata.time) {
@ -208,260 +160,16 @@ internal class RestoreViewModel(
mDisplayFragment.postEvent(RESTORE_BACKUP) mDisplayFragment.postEvent(RESTORE_BACKUP)
viewModelScope.launch(ioDispatcher) { viewModelScope.launch(ioDispatcher) {
startRestore() val backup = chosenRestorableBackup.value ?: error("No Backup chosen")
appDataRestoreManager.startRestore(backup)
} }
} }
@WorkerThread
private fun startRestore() {
val token = mChosenRestorableBackup.value?.token
?: throw IllegalStateException("No chosen backup")
Log.d(TAG, "Starting new restore session to restore backup $token")
// if we had no token before (i.e. restore from setup wizard),
// use the token of the current restore set from now on
if (settingsManager.getToken() == null) {
settingsManager.setNewToken(token)
}
// start a new restore session
val session = try {
getOrStartSession()
} catch (e: RemoteException) {
Log.e(TAG, "Error starting new session", e)
mRestoreBackupResult.postValue(
RestoreBackupResult(app.getString(R.string.restore_set_error))
)
return
}
val restorableBackup = mChosenRestorableBackup.value
val packages = restorableBackup?.packageMetadataMap?.keys?.toList()
?: run {
Log.e(TAG, "Chosen backup has empty package metadata map")
mRestoreBackupResult.postValue(
RestoreBackupResult(app.getString(R.string.restore_set_error))
)
return
}
val observer = RestoreObserver(
restoreCoordinator = restoreCoordinator,
restorableBackup = restorableBackup,
session = session,
packages = packages,
monitor = monitor
)
// We need to retrieve the restore sets before starting the restore.
// Otherwise, restorePackages() won't work as they need the restore sets cached internally.
if (session.getAvailableRestoreSets(observer, monitor) != 0) {
Log.e(TAG, "getAvailableRestoreSets() returned non-zero value")
mRestoreBackupResult.postValue(
RestoreBackupResult(app.getString(R.string.restore_set_error))
)
}
}
@WorkerThread
// this should be called one package at a time and never concurrently for different packages
private fun onRestoreStarted(packageName: String) {
// list is never null and always has at least one package
val list = mRestoreProgress.value!!
// check previous package first and change status
updateLatestPackage(list)
// add current package
list.addFirst(AppRestoreResult(packageName, getAppName(app, packageName), IN_PROGRESS))
mRestoreProgress.postValue(list)
}
@WorkerThread
private fun updateLatestPackage(list: LinkedList<AppRestoreResult>) {
val latestResult = list[0]
if (restoreCoordinator.isFailedPackage(latestResult.packageName)) {
list[0] = latestResult.copy(state = getFailedStatus(latestResult.packageName))
} else {
list[0] = latestResult.copy(state = SUCCEEDED)
}
}
@WorkerThread
private fun getFailedStatus(
packageName: String,
restorableBackup: RestorableBackup = chosenRestorableBackup.value!!,
): AppBackupState {
val metadata = restorableBackup.packageMetadataMap[packageName] ?: return FAILED
return when (metadata.state) {
NO_DATA -> FAILED_NO_DATA
WAS_STOPPED -> NOT_YET_BACKED_UP
NOT_ALLOWED -> FAILED_NOT_ALLOWED
QUOTA_EXCEEDED -> FAILED_QUOTA_EXCEEDED
UNKNOWN_ERROR -> FAILED
APK_AND_DATA -> {
if (app.packageManager.isInstalled(packageName)) FAILED else FAILED_NOT_INSTALLED
}
}
}
@WorkerThread
private fun onRestoreComplete(result: RestoreBackupResult) {
// update status of latest package
val list = mRestoreProgress.value!!
updateLatestPackage(list)
// add missing packages as failed
val seenPackages = list.map { it.packageName }
val restorableBackup = chosenRestorableBackup.value!!
val expectedPackages = restorableBackup.packageMetadataMap.keys
expectedPackages.removeAll(seenPackages)
for (packageName: String in expectedPackages) {
// TODO don't add if it was a NO_DATA system app
val failedStatus = getFailedStatus(packageName, restorableBackup)
list.addFirst(AppRestoreResult(packageName, getAppName(app, packageName), failedStatus))
}
mRestoreProgress.postValue(list)
mRestoreBackupResult.postValue(result)
}
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(ioDispatcher) { iconManager.removeIcons() } GlobalScope.launch(ioDispatcher) { iconManager.removeIcons() }
closeSession() appDataRestoreManager.closeSession()
}
private fun closeSession() {
session?.endRestoreSession()
session = null
}
@WorkerThread
private inner class RestoreObserver(
private val restoreCoordinator: RestoreCoordinator,
private val restorableBackup: RestorableBackup,
private val session: IRestoreSession,
private val packages: List<String>,
private val monitor: BackupMonitor,
) : IRestoreObserver.Stub() {
/**
* The current package index.
*
* Used for splitting the packages into chunks.
*/
private var packageIndex: Int = 0
/**
* Map of results for each chunk.
*
* The key is the chunk index, the value is the result.
*/
private val chunkResults = mutableMapOf<Int, Int>()
/**
* Supply a list of the restore datasets available from the current transport.
* This method is invoked as a callback following the application's use of the
* [IRestoreSession.getAvailableRestoreSets] method.
*
* @param restoreSets An array of [RestoreSet] objects
* describing all of the available datasets that are candidates for restoring to
* the current device. If no applicable datasets exist, restoreSets will be null.
*/
override fun restoreSetsAvailable(restoreSets: Array<out RestoreSet>?) {
// this gets executed after we got the restore sets
// now we can start the restore of all available packages
restoreNextPackages()
}
/**
* Restore the next chunk of packages.
*
* We need to restore in chunks, otherwise [BackupTransport.startRestore] in the
* framework's [PerformUnifiedRestoreTask] may fail due to an oversize Binder
* transaction, causing the entire restoration to fail.
*/
private fun restoreNextPackages() {
// Make sure metadata for selected backup is cached before starting each chunk.
val backupMetadata = restorableBackup.backupMetadata
restoreCoordinator.beforeStartRestore(backupMetadata)
val nextChunkIndex = (packageIndex + PACKAGES_PER_CHUNK).coerceAtMost(packages.size)
val packageChunk = packages.subList(packageIndex, nextChunkIndex).toTypedArray()
packageIndex += packageChunk.size
val token = backupMetadata.token
val result = session.restorePackages(token, this, packageChunk, monitor)
if (result != BackupManager.SUCCESS) {
Log.e(TAG, "restorePackages() returned non-zero value: $result")
}
}
/**
* The restore operation has begun.
*
* @param numPackages The total number of packages
* being processed in this restore operation.
*/
override fun restoreStarting(numPackages: Int) {
// noop
}
/**
* An indication of which package is being restored currently,
* out of the total number provided in the [restoreStarting] callback.
* This method is not guaranteed to be called.
*
* @param nowBeingRestored The index, between 1 and the numPackages parameter
* to the [restoreStarting] callback, of the package now being restored.
* @param currentPackage The name of the package now being restored.
*/
override fun onUpdate(nowBeingRestored: Int, currentPackage: String) {
// nowBeingRestored reporting is buggy, so don't use it
onRestoreStarted(currentPackage)
}
/**
* The restore operation has completed.
*
* @param result Zero on success; a nonzero error code if the restore operation
* as a whole failed.
*/
override fun restoreFinished(result: Int) {
val chunkIndex = packageIndex / PACKAGES_PER_CHUNK
chunkResults[chunkIndex] = result
// Restore next chunk if successful and there are more packages to restore.
if (packageIndex < packages.size) {
restoreNextPackages()
return
}
// Restore finished, time to get the result.
onRestoreComplete(getRestoreResult())
closeSession()
}
private fun getRestoreResult(): RestoreBackupResult {
val failedChunks = chunkResults
.filter { it.value != BackupManager.SUCCESS }
.map { "chunk ${it.key} failed with error ${it.value}" }
return if (failedChunks.isNotEmpty()) {
Log.e(TAG, "Restore failed: $failedChunks")
return RestoreBackupResult(
errorMsg = app.getString(R.string.restore_finished_error)
)
} else {
RestoreBackupResult(errorMsg = null)
}
}
} }
@UiThread @UiThread