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:
parent
66f3852edf
commit
b3f93adf77
2 changed files with 323 additions and 302 deletions
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue