Refactor fetching of restorable backups
so that we don't go through the BackupManager API, but use RestoreCoordinator directly
This commit is contained in:
parent
aeafc80bb9
commit
bcb245531c
3 changed files with 63 additions and 102 deletions
|
@ -1,19 +1,15 @@
|
||||||
package com.stevesoltys.seedvault.restore
|
package com.stevesoltys.seedvault.restore
|
||||||
|
|
||||||
import android.app.backup.RestoreSet
|
|
||||||
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||||
|
|
||||||
data class RestorableBackup(
|
data class RestorableBackup(private val backupMetadata: BackupMetadata) {
|
||||||
private val restoreSet: RestoreSet,
|
|
||||||
private val backupMetadata: BackupMetadata
|
|
||||||
) {
|
|
||||||
|
|
||||||
val name: String
|
val name: String
|
||||||
get() = restoreSet.name
|
get() = backupMetadata.deviceName
|
||||||
|
|
||||||
val token: Long
|
val token: Long
|
||||||
get() = restoreSet.token
|
get() = backupMetadata.token
|
||||||
|
|
||||||
val time: Long
|
val time: Long
|
||||||
get() = backupMetadata.time
|
get() = backupMetadata.time
|
||||||
|
|
|
@ -20,7 +20,6 @@ import com.stevesoltys.seedvault.BackupMonitor
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
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.BackupMetadata
|
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
||||||
|
@ -65,9 +64,6 @@ import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_TIMESTA
|
||||||
import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_USER_ID
|
import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_USER_ID
|
||||||
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
|
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import kotlin.coroutines.Continuation
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
private val TAG = RestoreViewModel::class.java.simpleName
|
private val TAG = RestoreViewModel::class.java.simpleName
|
||||||
|
|
||||||
|
@ -126,8 +122,6 @@ internal class RestoreViewModel(
|
||||||
|
|
||||||
@Throws(RemoteException::class)
|
@Throws(RemoteException::class)
|
||||||
private fun getOrStartSession(): IRestoreSession {
|
private fun getOrStartSession(): IRestoreSession {
|
||||||
// TODO consider not using the BackupManager for this, but our own API directly
|
|
||||||
// this is less error-prone (hanging sessions) and can provide more data
|
|
||||||
val session = this.session
|
val session = this.session
|
||||||
?: backupManager.beginRestoreSessionForUser(UserHandle.myUserId(), null, TRANSPORT_ID)
|
?: backupManager.beginRestoreSessionForUser(UserHandle.myUserId(), null, TRANSPORT_ID)
|
||||||
?: throw RemoteException("beginRestoreSessionForUser returned null")
|
?: throw RemoteException("beginRestoreSessionForUser returned null")
|
||||||
|
@ -135,36 +129,27 @@ internal class RestoreViewModel(
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun loadRestoreSets() = viewModelScope.launch {
|
internal fun loadRestoreSets() = viewModelScope.launch(ioDispatcher) {
|
||||||
mRestoreSetResults.value = getAvailableRestoreSets()
|
val backups = restoreCoordinator.getAvailableMetadata()?.mapNotNull { (token, metadata) ->
|
||||||
|
when (metadata.time) {
|
||||||
|
0L -> {
|
||||||
|
Log.d(TAG, "Ignoring RestoreSet with no last backup time: $token.")
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
else -> RestorableBackup(metadata)
|
||||||
private suspend fun getAvailableRestoreSets() =
|
|
||||||
suspendCoroutine<RestoreSetResult> { continuation ->
|
|
||||||
val session = try {
|
|
||||||
getOrStartSession()
|
|
||||||
} catch (e: RemoteException) {
|
|
||||||
Log.e(TAG, "Error starting new session", e)
|
|
||||||
continuation.resume(RestoreSetResult(app.getString(R.string.restore_set_error)))
|
|
||||||
return@suspendCoroutine
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val observer = RestoreObserver(continuation)
|
|
||||||
val setResult = session.getAvailableRestoreSets(observer, monitor)
|
|
||||||
if (setResult != 0) {
|
|
||||||
Log.e(TAG, "getAvailableRestoreSets() returned non-zero value")
|
|
||||||
continuation.resume(RestoreSetResult(app.getString(R.string.restore_set_error)))
|
|
||||||
return@suspendCoroutine
|
|
||||||
}
|
}
|
||||||
|
val result = when {
|
||||||
|
backups == null -> RestoreSetResult(app.getString(R.string.restore_set_error))
|
||||||
|
backups.isEmpty() -> RestoreSetResult(app.getString(R.string.restore_set_empty_result))
|
||||||
|
else -> RestoreSetResult(backups)
|
||||||
|
}
|
||||||
|
mRestoreSetResults.postValue(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestorableBackupClicked(restorableBackup: RestorableBackup) {
|
override fun onRestorableBackupClicked(restorableBackup: RestorableBackup) {
|
||||||
mChosenRestorableBackup.value = restorableBackup
|
mChosenRestorableBackup.value = restorableBackup
|
||||||
mDisplayFragment.setEvent(RESTORE_APPS)
|
mDisplayFragment.setEvent(RESTORE_APPS)
|
||||||
|
|
||||||
// re-installing apps will take some time and the session probably times out
|
|
||||||
// so better close it cleanly and re-open it later
|
|
||||||
closeSession()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getInstallResult(backup: RestorableBackup): LiveData<InstallResult> {
|
private fun getInstallResult(backup: RestorableBackup): LiveData<InstallResult> {
|
||||||
|
@ -193,7 +178,7 @@ internal class RestoreViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private suspend fun startRestore(token: Long) {
|
private fun startRestore(token: Long) {
|
||||||
Log.d(TAG, "Starting new restore session to restore backup $token")
|
Log.d(TAG, "Starting new restore session to restore backup $token")
|
||||||
|
|
||||||
// if we had no token before (i.e. restore from setup wizard),
|
// if we had no token before (i.e. restore from setup wizard),
|
||||||
|
@ -202,25 +187,35 @@ internal class RestoreViewModel(
|
||||||
settingsManager.setNewToken(token)
|
settingsManager.setNewToken(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
// we need to start a new session and retrieve the restore sets before starting the restore
|
// start a new restore session
|
||||||
val restoreSetResult = getAvailableRestoreSets()
|
val session = try {
|
||||||
if (restoreSetResult.hasError()) {
|
getOrStartSession()
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
Log.e(TAG, "Error starting new session", e)
|
||||||
mRestoreBackupResult.postValue(
|
mRestoreBackupResult.postValue(
|
||||||
RestoreBackupResult(app.getString(R.string.restore_finished_error))
|
RestoreBackupResult(app.getString(R.string.restore_set_error))
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we need to retrieve the restore sets before starting the restore
|
||||||
|
// otherwise restoreAll() won't work as they need the restore sets cached internally
|
||||||
|
val observer = RestoreObserver { observer ->
|
||||||
|
// this lambda gets executed after we got the restore sets
|
||||||
// now we can start the restore of all available packages
|
// now we can start the restore of all available packages
|
||||||
val observer = RestoreObserver()
|
val restoreAllResult = session.restoreAll(token, observer, monitor)
|
||||||
val restoreAllResult = session?.restoreAll(token, observer, monitor) ?: 1
|
|
||||||
if (restoreAllResult != 0) {
|
if (restoreAllResult != 0) {
|
||||||
if (session == null) Log.e(TAG, "session was null")
|
Log.e(TAG, "restoreAll() returned non-zero value")
|
||||||
else Log.e(TAG, "restoreAll() returned non-zero value")
|
|
||||||
mRestoreBackupResult.postValue(
|
mRestoreBackupResult.postValue(
|
||||||
RestoreBackupResult(app.getString(R.string.restore_finished_error))
|
RestoreBackupResult(app.getString(R.string.restore_finished_error))
|
||||||
)
|
)
|
||||||
return
|
}
|
||||||
|
}
|
||||||
|
if (session.getAvailableRestoreSets(observer, monitor) != 0) {
|
||||||
|
Log.e(TAG, "getAvailableRestoreSets() returned non-zero value")
|
||||||
|
mRestoreBackupResult.postValue(
|
||||||
|
RestoreBackupResult(app.getString(R.string.restore_set_error))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,9 +292,8 @@ internal class RestoreViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private inner class RestoreObserver(
|
private inner class RestoreObserver(private val startRestore: (RestoreObserver) -> Unit) :
|
||||||
private val continuation: Continuation<RestoreSetResult>? = null
|
IRestoreObserver.Stub() {
|
||||||
) : IRestoreObserver.Stub() {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supply a list of the restore datasets available from the current transport.
|
* Supply a list of the restore datasets available from the current transport.
|
||||||
|
@ -311,39 +305,7 @@ internal class RestoreViewModel(
|
||||||
* the current device. If no applicable datasets exist, restoreSets will be null.
|
* the current device. If no applicable datasets exist, restoreSets will be null.
|
||||||
*/
|
*/
|
||||||
override fun restoreSetsAvailable(restoreSets: Array<out RestoreSet>?) {
|
override fun restoreSetsAvailable(restoreSets: Array<out RestoreSet>?) {
|
||||||
check(continuation != null) { "Getting restore sets without continuation" }
|
startRestore(this)
|
||||||
|
|
||||||
val result = if (restoreSets == null || restoreSets.isEmpty()) {
|
|
||||||
RestoreSetResult(app.getString(R.string.restore_set_empty_result))
|
|
||||||
} else {
|
|
||||||
val backupMetadata = restoreCoordinator.getAndClearBackupMetadata()
|
|
||||||
if (backupMetadata == null) {
|
|
||||||
Log.e(TAG, "RestoreCoordinator#getAndClearBackupMetadata() returned null")
|
|
||||||
RestoreSetResult(app.getString(R.string.restore_set_error))
|
|
||||||
} else {
|
|
||||||
val restorableBackups = restoreSets.mapNotNull { set ->
|
|
||||||
getRestorableBackup(set, backupMetadata[set.token])
|
|
||||||
}
|
|
||||||
if (restorableBackups.isEmpty()) {
|
|
||||||
RestoreSetResult(app.getString(R.string.restore_set_empty_result))
|
|
||||||
} else RestoreSetResult(restorableBackups)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continuation.resume(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRestorableBackup(set: RestoreSet, metadata: BackupMetadata?) = when {
|
|
||||||
metadata == null -> {
|
|
||||||
Log.e(TAG, "No metadata for token ${set.token}.")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
metadata.time == 0L -> {
|
|
||||||
Log.d(TAG, "Ignoring RestoreSet with no last backup time: ${set.token}.")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
RestorableBackup(set, metadata)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -54,16 +54,9 @@ internal class RestoreCoordinator(
|
||||||
private var backupMetadata: LongSparseArray<BackupMetadata>? = null
|
private var backupMetadata: LongSparseArray<BackupMetadata>? = null
|
||||||
private val failedPackages = ArrayList<String>()
|
private val failedPackages = ArrayList<String>()
|
||||||
|
|
||||||
/**
|
suspend fun getAvailableMetadata(): Map<Long, BackupMetadata>? {
|
||||||
* Get the set of all backups currently available over this transport.
|
|
||||||
*
|
|
||||||
* @return Descriptions of the set of restore images available for this device,
|
|
||||||
* or null if an error occurred (the attempt should be rescheduled).
|
|
||||||
**/
|
|
||||||
suspend fun getAvailableRestoreSets(): Array<RestoreSet>? {
|
|
||||||
val availableBackups = plugin.getAvailableBackups() ?: return null
|
val availableBackups = plugin.getAvailableBackups() ?: return null
|
||||||
val restoreSets = ArrayList<RestoreSet>()
|
val metadataMap = HashMap<Long, BackupMetadata>()
|
||||||
val metadataMap = LongSparseArray<BackupMetadata>()
|
|
||||||
for (encryptedMetadata in availableBackups) {
|
for (encryptedMetadata in availableBackups) {
|
||||||
if (encryptedMetadata.error) continue
|
if (encryptedMetadata.error) continue
|
||||||
check(encryptedMetadata.inputStream != null) {
|
check(encryptedMetadata.inputStream != null) {
|
||||||
|
@ -74,9 +67,7 @@ internal class RestoreCoordinator(
|
||||||
encryptedMetadata.inputStream,
|
encryptedMetadata.inputStream,
|
||||||
encryptedMetadata.token
|
encryptedMetadata.token
|
||||||
)
|
)
|
||||||
metadataMap.put(encryptedMetadata.token, metadata)
|
metadataMap[encryptedMetadata.token] = metadata
|
||||||
val set = RestoreSet(metadata.deviceName, metadata.deviceName, metadata.token)
|
|
||||||
restoreSets.add(set)
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error while getting restore set ${encryptedMetadata.token}", e)
|
Log.e(TAG, "Error while getting restore set ${encryptedMetadata.token}", e)
|
||||||
continue
|
continue
|
||||||
|
@ -93,9 +84,20 @@ internal class RestoreCoordinator(
|
||||||
closeQuietly(encryptedMetadata.inputStream)
|
closeQuietly(encryptedMetadata.inputStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i(TAG, "Got available restore sets: $restoreSets")
|
Log.i(TAG, "Got available metadata for tokens: ${metadataMap.keys}")
|
||||||
this.backupMetadata = metadataMap
|
return metadataMap
|
||||||
return restoreSets.toTypedArray()
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the set of all backups currently available over this transport.
|
||||||
|
*
|
||||||
|
* @return Descriptions of the set of restore images available for this device,
|
||||||
|
* or null if an error occurred (the attempt should be rescheduled).
|
||||||
|
**/
|
||||||
|
suspend fun getAvailableRestoreSets(): Array<RestoreSet>? {
|
||||||
|
return getAvailableMetadata()?.map { (_, metadata) ->
|
||||||
|
RestoreSet(metadata.deviceName, metadata.deviceName, metadata.token)
|
||||||
|
}?.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -211,7 +213,8 @@ internal class RestoreCoordinator(
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error finding restore data for $packageName.", e)
|
Log.e(TAG, "Error finding restore data for $packageName.", e)
|
||||||
failedPackages.add(packageName)
|
failedPackages.add(packageName)
|
||||||
return null
|
// don't return null and cause abort here, but try next package
|
||||||
|
return nextRestorePackage()
|
||||||
}
|
}
|
||||||
return RestoreDescription(packageName, type)
|
return RestoreDescription(packageName, type)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue