Prepare restore backup loading for v2
This commit is contained in:
parent
8ce79f4195
commit
83708d9403
20 changed files with 274 additions and 168 deletions
|
@ -10,7 +10,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||
import androidx.test.filters.MediumTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.stevesoltys.seedvault.backend.LegacyStoragePlugin
|
||||
import com.stevesoltys.seedvault.backend.getAvailableBackups
|
||||
import com.stevesoltys.seedvault.backend.getAvailableBackupFileHandles
|
||||
import com.stevesoltys.seedvault.backend.saf.DocumentsProviderLegacyPlugin
|
||||
import com.stevesoltys.seedvault.backend.saf.DocumentsStorage
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
|
@ -92,7 +92,7 @@ class PluginTest : KoinComponent {
|
|||
@Test
|
||||
fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) {
|
||||
// no backups available initially
|
||||
assertEquals(0, backend.getAvailableBackups()?.toList()?.size)
|
||||
assertEquals(0, backend.getAvailableBackupFileHandles().toList().size)
|
||||
|
||||
// prepare returned tokens requested when initializing device
|
||||
every { mockedSettingsManager.getToken() } returnsMany listOf(token, token + 1, token + 1)
|
||||
|
@ -102,17 +102,17 @@ class PluginTest : KoinComponent {
|
|||
.writeAndClose(getRandomByteArray())
|
||||
|
||||
// one backup available now
|
||||
assertEquals(1, backend.getAvailableBackups()?.toList()?.size)
|
||||
assertEquals(1, backend.getAvailableBackupFileHandles().toList().size)
|
||||
|
||||
// initializing again (with another restore set) does add a restore set
|
||||
backend.save(LegacyAppBackupFile.Metadata(token + 1))
|
||||
.writeAndClose(getRandomByteArray())
|
||||
assertEquals(2, backend.getAvailableBackups()?.toList()?.size)
|
||||
assertEquals(2, backend.getAvailableBackupFileHandles().toList().size)
|
||||
|
||||
// initializing again (without new restore set) doesn't change number of restore sets
|
||||
backend.save(LegacyAppBackupFile.Metadata(token + 1))
|
||||
.writeAndClose(getRandomByteArray())
|
||||
assertEquals(2, backend.getAvailableBackups()?.toList()?.size)
|
||||
assertEquals(2, backend.getAvailableBackupFileHandles().toList().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -124,27 +124,26 @@ class PluginTest : KoinComponent {
|
|||
backend.save(LegacyAppBackupFile.Metadata(token)).writeAndClose(metadata)
|
||||
|
||||
// get available backups, expect only one with our token and no error
|
||||
var availableBackups = backend.getAvailableBackups()?.toList()
|
||||
check(availableBackups != null)
|
||||
var availableBackups = backend.getAvailableBackupFileHandles().toList()
|
||||
assertEquals(1, availableBackups.size)
|
||||
assertEquals(token, availableBackups[0].token)
|
||||
var backupHandle = availableBackups[0] as LegacyAppBackupFile.Metadata
|
||||
assertEquals(token, backupHandle.token)
|
||||
|
||||
// read metadata matches what was written earlier
|
||||
assertReadEquals(metadata, availableBackups[0].inputStreamRetriever())
|
||||
assertReadEquals(metadata, backend.load(backupHandle))
|
||||
|
||||
// initializing again (without changing storage) keeps restore set with same token
|
||||
backend.save(LegacyAppBackupFile.Metadata(token)).writeAndClose(metadata)
|
||||
availableBackups = backend.getAvailableBackups()?.toList()
|
||||
check(availableBackups != null)
|
||||
availableBackups = backend.getAvailableBackupFileHandles().toList()
|
||||
assertEquals(1, availableBackups.size)
|
||||
assertEquals(token, availableBackups[0].token)
|
||||
backupHandle = availableBackups[0] as LegacyAppBackupFile.Metadata
|
||||
assertEquals(token, backupHandle.token)
|
||||
|
||||
// metadata hasn't changed
|
||||
assertReadEquals(metadata, availableBackups[0].inputStreamRetriever())
|
||||
assertReadEquals(metadata, backend.load(backupHandle))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("Deprecation")
|
||||
fun v0testApkWriteRead() = runBlocking {
|
||||
// initialize storage with given token
|
||||
initStorage(token)
|
||||
|
|
|
@ -5,37 +5,29 @@
|
|||
|
||||
package com.stevesoltys.seedvault.backend
|
||||
|
||||
import android.util.Log
|
||||
import at.bitfire.dav4jvm.exception.HttpException
|
||||
import org.calyxos.seedvault.core.backends.AppBackupFileType
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.FileHandle
|
||||
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
suspend fun Backend.getMetadataOutputStream(token: Long): OutputStream {
|
||||
return save(LegacyAppBackupFile.Metadata(token))
|
||||
}
|
||||
|
||||
suspend fun Backend.getAvailableBackups(): Sequence<EncryptedMetadata>? {
|
||||
return try {
|
||||
// get all restore set tokens in root folder that have a metadata file
|
||||
val handles = ArrayList<LegacyAppBackupFile.Metadata>()
|
||||
list(null, LegacyAppBackupFile.Metadata::class) { fileInfo ->
|
||||
val handle = fileInfo.fileHandle as LegacyAppBackupFile.Metadata
|
||||
handles.add(handle)
|
||||
suspend fun Backend.getAvailableBackupFileHandles(): List<FileHandle> {
|
||||
// v1 get all restore set tokens in root folder that have a metadata file
|
||||
// v2 get all snapshots in all repository folders
|
||||
return ArrayList<FileHandle>().apply {
|
||||
list(
|
||||
null,
|
||||
AppBackupFileType.Snapshot::class,
|
||||
LegacyAppBackupFile.Metadata::class,
|
||||
) { fileInfo ->
|
||||
add(fileInfo.fileHandle)
|
||||
}
|
||||
val handleIterator = handles.iterator()
|
||||
return generateSequence {
|
||||
if (!handleIterator.hasNext()) return@generateSequence null // end sequence
|
||||
val handle = handleIterator.next()
|
||||
EncryptedMetadata(handle.token) {
|
||||
load(handle)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("SafBackend", "Error getting available backups: ", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,5 +41,3 @@ fun Exception.isOutOfSpace(): Boolean {
|
|||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
class EncryptedMetadata(val token: Long, val inputStreamRetriever: suspend () -> InputStream)
|
||||
|
|
|
@ -15,7 +15,7 @@ import android.util.Log
|
|||
import androidx.annotation.WorkerThread
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.getAvailableBackups
|
||||
import com.stevesoltys.seedvault.backend.getAvailableBackupFileHandles
|
||||
import com.stevesoltys.seedvault.isMassStorage
|
||||
import com.stevesoltys.seedvault.settings.FlashDrive
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
|
@ -59,9 +59,8 @@ internal class SafHandler(
|
|||
@WorkerThread
|
||||
@Throws(IOException::class)
|
||||
suspend fun hasAppBackup(safProperties: SafProperties): Boolean {
|
||||
val appPlugin = backendFactory.createSafBackend(safProperties)
|
||||
val backups = appPlugin.getAvailableBackups()
|
||||
return backups != null && backups.iterator().hasNext()
|
||||
val backend = backendFactory.createSafBackend(safProperties)
|
||||
return backend.getAvailableBackupFileHandles().isNotEmpty()
|
||||
}
|
||||
|
||||
fun save(safProperties: SafProperties) {
|
||||
|
|
|
@ -10,7 +10,7 @@ import android.util.Log
|
|||
import androidx.annotation.WorkerThread
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.getAvailableBackups
|
||||
import com.stevesoltys.seedvault.backend.getAvailableBackupFileHandles
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
@ -81,8 +81,7 @@ internal class WebDavHandler(
|
|||
@WorkerThread
|
||||
@Throws(IOException::class)
|
||||
suspend fun hasAppBackup(backend: Backend): Boolean {
|
||||
val backups = backend.getAvailableBackups()
|
||||
return backups != null && backups.iterator().hasNext()
|
||||
return backend.getAvailableBackupFileHandles().isNotEmpty()
|
||||
}
|
||||
|
||||
fun save(properties: WebDavProperties) {
|
||||
|
|
|
@ -30,6 +30,23 @@ data class BackupMetadata(
|
|||
internal var d2dBackup: Boolean = false,
|
||||
internal val packageMetadataMap: PackageMetadataMap = PackageMetadataMap(),
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun fromSnapshot(s: Snapshot) = BackupMetadata(
|
||||
version = s.version.toByte(),
|
||||
token = s.token,
|
||||
salt = "",
|
||||
time = s.token,
|
||||
androidVersion = s.sdkInt,
|
||||
androidIncremental = s.androidIncremental,
|
||||
deviceName = s.name,
|
||||
d2dBackup = s.d2D,
|
||||
packageMetadataMap = s.appsMap.mapValues { (_, app) ->
|
||||
PackageMetadata.fromSnapshot(app)
|
||||
} as PackageMetadataMap
|
||||
)
|
||||
}
|
||||
|
||||
val size: Long
|
||||
get() = packageMetadataMap.values.sumOf { m ->
|
||||
(m.size ?: 0L) + (m.splits?.sumOf { it.size ?: 0L } ?: 0L)
|
||||
|
|
|
@ -23,12 +23,13 @@ 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.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||
import com.stevesoltys.seedvault.metadata.PackageState
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
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.RestorableBackup
|
||||
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
|
||||
import com.stevesoltys.seedvault.ui.AppBackupState
|
||||
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED
|
||||
|
@ -263,20 +264,19 @@ internal class AppDataRestoreManager(
|
|||
/**
|
||||
* 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.
|
||||
* We need to restore packages in chunks, otherwise [BackupTransport.startRestore] in the
|
||||
* framework's [PerformUnifiedRestoreTask] may fail due to an oversize Binder transaction,
|
||||
* causing the entire restoration to fail due to too many package names.
|
||||
*/
|
||||
private fun restoreNextPackages() {
|
||||
// Make sure metadata for selected backup is cached before starting each chunk.
|
||||
val backupMetadata = restorableBackup.backupMetadata
|
||||
restoreCoordinator.beforeStartRestore(backupMetadata)
|
||||
restoreCoordinator.beforeStartRestore(restorableBackup)
|
||||
|
||||
val nextChunkIndex = (packageIndex + PACKAGES_PER_CHUNK).coerceAtMost(packages.size)
|
||||
val packageChunk = packages.subList(packageIndex, nextChunkIndex).toTypedArray()
|
||||
packageIndex += packageChunk.size
|
||||
|
||||
val token = backupMetadata.token
|
||||
val token = restorableBackup.token
|
||||
val result = session.restorePackages(token, this, packageChunk, monitor)
|
||||
|
||||
@Suppress("UNRESOLVED_REFERENCE") // BackupManager.SUCCESS
|
||||
|
|
|
@ -15,6 +15,7 @@ import com.stevesoltys.seedvault.R
|
|||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
|
||||
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SYSTEM
|
||||
import com.stevesoltys.seedvault.ui.systemData
|
||||
import com.stevesoltys.seedvault.worker.IconManager
|
||||
|
@ -68,21 +69,23 @@ internal class AppSelectionManager(
|
|||
val name = context.getString(data.nameRes)
|
||||
SelectableAppItem(packageName, metadata.copy(name = name), true)
|
||||
}
|
||||
val systemItem = SelectableAppItem(
|
||||
packageName = PACKAGE_NAME_SYSTEM,
|
||||
metadata = PackageMetadata(
|
||||
time = restorableBackup.packageMetadataMap.values.maxOf {
|
||||
if (it.system) it.time else -1
|
||||
},
|
||||
size = restorableBackup.packageMetadataMap.values.sumOf {
|
||||
if (it.system) it.size ?: 0L else 0L
|
||||
},
|
||||
system = true,
|
||||
name = context.getString(R.string.backup_system_apps),
|
||||
),
|
||||
selected = isSetupWizard,
|
||||
)
|
||||
items.add(0, systemItem)
|
||||
if (restorableBackup.packageMetadataMap.isNotEmpty()) {
|
||||
val systemItem = SelectableAppItem(
|
||||
packageName = PACKAGE_NAME_SYSTEM,
|
||||
metadata = PackageMetadata(
|
||||
time = restorableBackup.packageMetadataMap.values.maxOf {
|
||||
if (it.system) it.time else -1
|
||||
},
|
||||
size = restorableBackup.packageMetadataMap.values.sumOf {
|
||||
if (it.system) it.size ?: 0L else 0L
|
||||
},
|
||||
system = true,
|
||||
name = context.getString(R.string.backup_system_apps),
|
||||
),
|
||||
selected = isSetupWizard,
|
||||
)
|
||||
items.add(0, systemItem)
|
||||
}
|
||||
items.addAll(0, systemDataItems)
|
||||
selectedApps.value =
|
||||
SelectedAppsState(apps = items, allSelected = isSetupWizard, iconsLoaded = false)
|
||||
|
|
|
@ -19,6 +19,7 @@ import androidx.recyclerview.widget.RecyclerView.Adapter
|
|||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.restore.RestoreSetAdapter.RestoreSetViewHolder
|
||||
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
|
||||
|
||||
internal class RestoreSetAdapter(
|
||||
private val listener: RestorableBackupClickListener,
|
||||
|
|
|
@ -17,6 +17,7 @@ import android.widget.TextView
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
|
||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||
|
||||
class RestoreSetFragment : Fragment() {
|
||||
|
|
|
@ -9,7 +9,6 @@ import android.app.Application
|
|||
import android.app.backup.IBackupManager
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.appcompat.content.res.AppCompatResources.getDrawable
|
||||
import androidx.lifecycle.LiveData
|
||||
|
@ -17,8 +16,8 @@ import androidx.lifecycle.MutableLiveData
|
|||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
|
||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
|
||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
|
||||
|
@ -30,6 +29,9 @@ import com.stevesoltys.seedvault.restore.install.InstallIntentCreator
|
|||
import com.stevesoltys.seedvault.restore.install.InstallResult
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.storage.StorageRestoreService
|
||||
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
|
||||
import com.stevesoltys.seedvault.transport.restore.RestorableBackupResult.ErrorResult
|
||||
import com.stevesoltys.seedvault.transport.restore.RestorableBackupResult.SuccessResult
|
||||
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
|
||||
import com.stevesoltys.seedvault.ui.LiveEvent
|
||||
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
||||
|
@ -106,20 +108,11 @@ internal class RestoreViewModel(
|
|||
private var storedSnapshot: StoredSnapshot? = null
|
||||
|
||||
internal fun loadRestoreSets() = viewModelScope.launch(ioDispatcher) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
val result = when (val backups = restoreCoordinator.getAvailableBackups()) {
|
||||
is ErrorResult -> RestoreSetResult(
|
||||
app.getString(R.string.restore_set_error) + "\n\n${backups.e}"
|
||||
)
|
||||
is SuccessResult -> RestoreSetResult(backups.backups)
|
||||
}
|
||||
mRestoreSetResults.postValue(result)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import com.stevesoltys.seedvault.crypto.Crypto
|
|||
import com.stevesoltys.seedvault.encodeBase64
|
||||
import com.stevesoltys.seedvault.metadata.ApkSplit
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.restore.RestorableBackup
|
||||
import com.stevesoltys.seedvault.restore.RestoreService
|
||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
|
||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
|
||||
|
|
|
@ -1,20 +1,31 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020 The Calyx Institute
|
||||
* SPDX-FileCopyrightText: 2024 The Calyx Institute
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.stevesoltys.seedvault.restore
|
||||
package com.stevesoltys.seedvault.transport.restore
|
||||
|
||||
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||
import com.stevesoltys.seedvault.proto.Snapshot
|
||||
|
||||
sealed class RestorableBackupResult {
|
||||
data class ErrorResult(val e: Exception?) : RestorableBackupResult()
|
||||
data class SuccessResult(val backups: List<RestorableBackup>) : RestorableBackupResult()
|
||||
}
|
||||
|
||||
data class RestorableBackup(
|
||||
val backupMetadata: BackupMetadata,
|
||||
val repoId: String? = null,
|
||||
val snapshot: Snapshot? = null,
|
||||
) {
|
||||
|
||||
constructor(repoId: String, snapshot: Snapshot) : this(
|
||||
backupMetadata = BackupMetadata.fromSnapshot(snapshot),
|
||||
repoId = repoId,
|
||||
snapshot = snapshot,
|
||||
)
|
||||
|
||||
val name: String
|
||||
get() = backupMetadata.deviceName
|
||||
|
||||
|
@ -30,8 +41,9 @@ data class RestorableBackup(
|
|||
val time: Long
|
||||
get() = backupMetadata.time
|
||||
|
||||
val size: Long?
|
||||
get() = backupMetadata.size
|
||||
val size: Long
|
||||
get() = snapshot?.blobsMap?.values?.sumOf { it.uncompressedLength.toLong() }
|
||||
?: backupMetadata.size
|
||||
|
||||
val deviceName: String
|
||||
get() = backupMetadata.deviceName
|
|
@ -18,20 +18,22 @@ import android.os.ParcelFileDescriptor
|
|||
import android.util.Log
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.getAvailableBackupFileHandles
|
||||
import com.stevesoltys.seedvault.crypto.Crypto
|
||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
||||
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
||||
import com.stevesoltys.seedvault.metadata.BackupType
|
||||
import com.stevesoltys.seedvault.metadata.DecryptionFailedException
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.metadata.MetadataReader
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.getAvailableBackups
|
||||
import com.stevesoltys.seedvault.proto.Snapshot
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS
|
||||
import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS
|
||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||
import org.calyxos.seedvault.core.backends.AppBackupFileType
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
|
@ -49,7 +51,7 @@ private data class RestoreCoordinatorState(
|
|||
* Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@
|
||||
*/
|
||||
val autoRestorePackageInfo: PackageInfo?,
|
||||
val backupMetadata: BackupMetadata,
|
||||
val backup: RestorableBackup,
|
||||
) {
|
||||
var currentPackage: String? = null
|
||||
}
|
||||
|
@ -63,6 +65,7 @@ internal class RestoreCoordinator(
|
|||
private val metadataManager: MetadataManager,
|
||||
private val notificationManager: BackupNotificationManager,
|
||||
private val backendManager: BackendManager,
|
||||
private val loader: Loader,
|
||||
private val kv: KVRestore,
|
||||
private val full: FullRestore,
|
||||
private val metadataReader: MetadataReader,
|
||||
|
@ -70,34 +73,58 @@ internal class RestoreCoordinator(
|
|||
|
||||
private val backend: Backend get() = backendManager.backend
|
||||
private var state: RestoreCoordinatorState? = null
|
||||
private var backupMetadata: BackupMetadata? = null
|
||||
private var restorableBackup: RestorableBackup? = null
|
||||
private val failedPackages = ArrayList<String>()
|
||||
|
||||
suspend fun getAvailableMetadata(): Map<Long, BackupMetadata>? {
|
||||
val availableBackups = backend.getAvailableBackups() ?: return null
|
||||
val metadataMap = HashMap<Long, BackupMetadata>()
|
||||
for (encryptedMetadata in availableBackups) {
|
||||
suspend fun getAvailableBackups(): RestorableBackupResult {
|
||||
val fileHandles = try {
|
||||
backend.getAvailableBackupFileHandles()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error getting available backups.", e)
|
||||
return RestorableBackupResult.ErrorResult(e)
|
||||
}
|
||||
val backups = ArrayList<RestorableBackup>()
|
||||
var lastException: Exception? = null
|
||||
for (handle in fileHandles) {
|
||||
try {
|
||||
val metadata = encryptedMetadata.inputStreamRetriever().use { inputStream ->
|
||||
metadataReader.readMetadata(inputStream, encryptedMetadata.token)
|
||||
val backup = when (handle) {
|
||||
is AppBackupFileType.Snapshot -> {
|
||||
val snapshot = loader.loadFile(handle).use { inputStream ->
|
||||
Snapshot.parseFrom(inputStream)
|
||||
}
|
||||
RestorableBackup(
|
||||
repoId = handle.repoId,
|
||||
snapshot = snapshot,
|
||||
)
|
||||
}
|
||||
is LegacyAppBackupFile.Metadata -> {
|
||||
val metadata = backend.load(handle).use { inputStream ->
|
||||
metadataReader.readMetadata(inputStream, handle.token)
|
||||
}
|
||||
RestorableBackup(backupMetadata = metadata)
|
||||
}
|
||||
else -> error("Unexpected file handle: $handle")
|
||||
}
|
||||
metadataMap[encryptedMetadata.token] = metadata
|
||||
backups.add(backup)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error while getting restore set ${encryptedMetadata.token}", e)
|
||||
Log.e(TAG, "Error while getting restore set $handle", e)
|
||||
lastException = e
|
||||
continue
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Error while getting restore set ${encryptedMetadata.token}", e)
|
||||
return null
|
||||
Log.e(TAG, "Error while getting restore set $handle", e)
|
||||
return RestorableBackupResult.ErrorResult(e)
|
||||
} catch (e: DecryptionFailedException) {
|
||||
Log.e(TAG, "Error while decrypting restore set ${encryptedMetadata.token}", e)
|
||||
Log.e(TAG, "Error while decrypting restore set $handle", e)
|
||||
lastException = e
|
||||
continue
|
||||
} catch (e: UnsupportedVersionException) {
|
||||
Log.w(TAG, "Backup with unsupported version read", e)
|
||||
lastException = e
|
||||
continue
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "Got available metadata for tokens: ${metadataMap.keys}")
|
||||
return metadataMap
|
||||
if (backups.isEmpty()) return RestorableBackupResult.ErrorResult(lastException)
|
||||
return RestorableBackupResult.SuccessResult(backups)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -107,22 +134,21 @@ internal class RestoreCoordinator(
|
|||
* or null if an error occurred (the attempt should be rescheduled).
|
||||
**/
|
||||
suspend fun getAvailableRestoreSets(): Array<RestoreSet>? {
|
||||
return getAvailableMetadata()?.map { (_, metadata) ->
|
||||
|
||||
val transportFlags = if (metadata.d2dBackup) {
|
||||
val result = getAvailableBackups() as? RestorableBackupResult.SuccessResult ?: return null
|
||||
val backups = result.backups
|
||||
return backups.map { backup ->
|
||||
val transportFlags = if (backup.d2dBackup) {
|
||||
D2D_TRANSPORT_FLAGS
|
||||
} else {
|
||||
DEFAULT_TRANSPORT_FLAGS
|
||||
}
|
||||
|
||||
val deviceName = if (metadata.d2dBackup) {
|
||||
val deviceName = if (backup.d2dBackup) {
|
||||
D2D_DEVICE_NAME
|
||||
} else {
|
||||
metadata.deviceName
|
||||
backup.deviceName
|
||||
}
|
||||
|
||||
RestoreSet(metadata.deviceName, deviceName, metadata.token, transportFlags)
|
||||
}?.toTypedArray()
|
||||
RestoreSet(backup.deviceName, deviceName, backup.token, transportFlags)
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -141,10 +167,10 @@ internal class RestoreCoordinator(
|
|||
/**
|
||||
* Call this before starting the restore as an optimization to prevent re-fetching metadata.
|
||||
*/
|
||||
fun beforeStartRestore(backupMetadata: BackupMetadata) {
|
||||
this.backupMetadata = backupMetadata
|
||||
fun beforeStartRestore(restorableBackup: RestorableBackup) {
|
||||
this.restorableBackup = restorableBackup
|
||||
|
||||
if (backupMetadata.d2dBackup) {
|
||||
if (restorableBackup.d2dBackup) {
|
||||
settingsManager.setD2dBackupsEnabled(true)
|
||||
}
|
||||
}
|
||||
|
@ -188,13 +214,15 @@ internal class RestoreCoordinator(
|
|||
packages[1]
|
||||
} else null
|
||||
|
||||
val metadata = if (backupMetadata?.token == token) {
|
||||
backupMetadata!! // if token matches, backupMetadata is non-null
|
||||
val backup = if (restorableBackup?.token == token) {
|
||||
restorableBackup!! // if token matches, backupMetadata is non-null
|
||||
} else {
|
||||
getAvailableMetadata()?.get(token) ?: return TRANSPORT_ERROR
|
||||
val backup = getAvailableBackups() as? RestorableBackupResult.SuccessResult
|
||||
?: return TRANSPORT_ERROR
|
||||
backup.backups.find { it.token == token } ?: return TRANSPORT_ERROR
|
||||
}
|
||||
state = RestoreCoordinatorState(token, packages.iterator(), pmPackageInfo, metadata)
|
||||
backupMetadata = null
|
||||
state = RestoreCoordinatorState(token, packages.iterator(), pmPackageInfo, backup)
|
||||
restorableBackup = null
|
||||
failedPackages.clear()
|
||||
return TRANSPORT_OK
|
||||
}
|
||||
|
@ -231,13 +259,13 @@ internal class RestoreCoordinator(
|
|||
|
||||
if (!state.packages.hasNext()) return NO_MORE_PACKAGES
|
||||
val packageInfo = state.packages.next()
|
||||
val version = state.backupMetadata.version
|
||||
val version = state.backup.version
|
||||
if (version == 0.toByte()) return nextRestorePackageV0(state, packageInfo)
|
||||
|
||||
val packageName = packageInfo.packageName
|
||||
val type = when (state.backupMetadata.packageMetadataMap[packageName]?.backupType) {
|
||||
val type = when (state.backup.packageMetadataMap[packageName]?.backupType) {
|
||||
BackupType.KV -> {
|
||||
val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName)
|
||||
val name = crypto.getNameForPackage(state.backup.salt, packageName)
|
||||
kv.initializeState(
|
||||
version = version,
|
||||
token = state.token,
|
||||
|
@ -250,7 +278,7 @@ internal class RestoreCoordinator(
|
|||
}
|
||||
|
||||
BackupType.FULL -> {
|
||||
val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName)
|
||||
val name = crypto.getNameForPackage(state.backup.salt, packageName)
|
||||
full.initializeState(version, state.token, name, packageInfo)
|
||||
state.currentPackage = packageName
|
||||
TYPE_FULL_STREAM
|
||||
|
@ -258,7 +286,7 @@ internal class RestoreCoordinator(
|
|||
|
||||
null -> {
|
||||
Log.i(TAG, "No backup type found for $packageName. Skipping...")
|
||||
state.backupMetadata.packageMetadataMap[packageName]?.backupType?.let { s ->
|
||||
state.backup.packageMetadataMap[packageName]?.backupType?.let { s ->
|
||||
Log.w(TAG, "State was ${s.name}")
|
||||
}
|
||||
failedPackages.add(packageName)
|
||||
|
|
|
@ -14,6 +14,17 @@ val restoreModule = module {
|
|||
single { KVRestore(get(), get(), get(), get(), get(), get()) }
|
||||
single { FullRestore(get(), get(), get(), get(), get()) }
|
||||
single {
|
||||
RestoreCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get(), get())
|
||||
RestoreCoordinator(
|
||||
context = androidContext(),
|
||||
crypto = get(),
|
||||
settingsManager = get(),
|
||||
metadataManager = get(),
|
||||
notificationManager = get(),
|
||||
backendManager = get(),
|
||||
loader = get(),
|
||||
kv = get(),
|
||||
full = get(),
|
||||
metadataReader = get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ import org.calyxos.seedvault.core.backends.Backend
|
|||
import org.calyxos.seedvault.core.backends.Constants.DIRECTORY_ROOT
|
||||
import org.calyxos.seedvault.core.backends.saf.SafProperties
|
||||
import org.calyxos.seedvault.core.backends.webdav.WebDavProperties
|
||||
import java.io.IOException
|
||||
|
||||
private val TAG = RestoreStorageViewModel::class.java.simpleName
|
||||
|
||||
|
@ -37,9 +36,11 @@ internal class RestoreStorageViewModel(
|
|||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val hasBackup = try {
|
||||
safHandler.hasAppBackup(safProperties)
|
||||
} catch (e: IOException) {
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error reading URI: ${safProperties.uri}", e)
|
||||
false
|
||||
val errorMsg = app.getString(R.string.restore_set_error) + "\n\n$e"
|
||||
mLocationChecked.postEvent(LocationResult(errorMsg))
|
||||
return@launch
|
||||
}
|
||||
if (hasBackup) {
|
||||
safHandler.save(safProperties)
|
||||
|
@ -60,9 +61,11 @@ internal class RestoreStorageViewModel(
|
|||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val hasBackup = try {
|
||||
webdavHandler.hasAppBackup(backend)
|
||||
} catch (e: IOException) {
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error reading: ${properties.config.url}", e)
|
||||
false
|
||||
val errorMsg = app.getString(R.string.restore_set_error) + "\n\n$e"
|
||||
mLocationChecked.postEvent(LocationResult(errorMsg))
|
||||
return@launch
|
||||
}
|
||||
if (hasBackup) {
|
||||
webdavHandler.save(properties)
|
||||
|
|
|
@ -15,6 +15,7 @@ import com.stevesoltys.seedvault.metadata.BackupMetadata
|
|||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||
import com.stevesoltys.seedvault.transport.TransportTest
|
||||
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
|
||||
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_CONTACTS
|
||||
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SETTINGS
|
||||
import com.stevesoltys.seedvault.ui.PACKAGE_NAME_SYSTEM
|
||||
|
@ -27,7 +28,6 @@ import kotlinx.coroutines.test.TestScope
|
|||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
|
@ -239,7 +239,7 @@ internal class AppSelectionManagerTest : TransportTest() {
|
|||
val backend: Backend = mockk()
|
||||
every { backendManager.backend } returns backend
|
||||
coEvery {
|
||||
backend.load(LegacyAppBackupFile.IconsFile(backupMetadata.token))
|
||||
iconManager.downloadIcons(repoId, snapshot)
|
||||
} throws IOException()
|
||||
|
||||
appSelectionManager.selectedAppsFlow.test {
|
||||
|
|
|
@ -29,7 +29,7 @@ import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
|||
import com.stevesoltys.seedvault.proto.SnapshotKt.blob
|
||||
import com.stevesoltys.seedvault.proto.SnapshotKt.split
|
||||
import com.stevesoltys.seedvault.proto.copy
|
||||
import com.stevesoltys.seedvault.restore.RestorableBackup
|
||||
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
|
||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
|
||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
|
||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
|
||||
|
|
|
@ -30,7 +30,9 @@ import com.stevesoltys.seedvault.transport.backup.PackageService
|
|||
import com.stevesoltys.seedvault.transport.backup.TestKvDbManager
|
||||
import com.stevesoltys.seedvault.transport.restore.FullRestore
|
||||
import com.stevesoltys.seedvault.transport.restore.KVRestore
|
||||
import com.stevesoltys.seedvault.transport.restore.Loader
|
||||
import com.stevesoltys.seedvault.transport.restore.OutputFactory
|
||||
import com.stevesoltys.seedvault.transport.restore.RestorableBackup
|
||||
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
|
||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||
import com.stevesoltys.seedvault.worker.ApkBackup
|
||||
|
@ -68,6 +70,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
@Suppress("Deprecation")
|
||||
private val legacyPlugin = mockk<LegacyStoragePlugin>()
|
||||
private val backend = mockk<Backend>()
|
||||
private val loader = mockk<Loader>()
|
||||
private val kvBackup = KVBackup(
|
||||
backendManager = backendManager,
|
||||
settingsManager = settingsManager,
|
||||
|
@ -114,11 +117,13 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
metadataManager,
|
||||
notificationManager,
|
||||
backendManager,
|
||||
loader,
|
||||
kvRestore,
|
||||
fullRestore,
|
||||
metadataReader
|
||||
)
|
||||
|
||||
private val restorableBackup = RestorableBackup(metadata)
|
||||
private val backupDataInput = mockk<BackupDataInput>()
|
||||
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
|
||||
private val appData = ByteArray(42).apply { Random.nextBytes(this) }
|
||||
|
@ -185,7 +190,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
assertEquals(TRANSPORT_OK, backup.finishBackup())
|
||||
|
||||
// start restore
|
||||
restore.beforeStartRestore(metadata)
|
||||
restore.beforeStartRestore(restorableBackup)
|
||||
assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo)))
|
||||
|
||||
// find data for K/V backup
|
||||
|
@ -262,7 +267,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
assertEquals(TRANSPORT_OK, backup.finishBackup())
|
||||
|
||||
// start restore
|
||||
restore.beforeStartRestore(metadata)
|
||||
restore.beforeStartRestore(restorableBackup)
|
||||
assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo)))
|
||||
|
||||
// find data for K/V backup
|
||||
|
@ -328,7 +333,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
assertEquals(TRANSPORT_OK, backup.finishBackup())
|
||||
|
||||
// start restore
|
||||
restore.beforeStartRestore(metadata)
|
||||
restore.beforeStartRestore(restorableBackup)
|
||||
assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo)))
|
||||
|
||||
// finds data for full backup
|
||||
|
|
|
@ -14,8 +14,6 @@ import android.app.backup.RestoreDescription.TYPE_KEY_VALUE
|
|||
import android.content.pm.PackageInfo
|
||||
import android.os.ParcelFileDescriptor
|
||||
import com.stevesoltys.seedvault.backend.BackendManager
|
||||
import com.stevesoltys.seedvault.backend.EncryptedMetadata
|
||||
import com.stevesoltys.seedvault.backend.getAvailableBackups
|
||||
import com.stevesoltys.seedvault.coAssertThrows
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.header.VERSION
|
||||
|
@ -32,7 +30,10 @@ import io.mockk.mockk
|
|||
import io.mockk.mockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.calyxos.seedvault.core.backends.AppBackupFileType
|
||||
import org.calyxos.seedvault.core.backends.Backend
|
||||
import org.calyxos.seedvault.core.backends.FileInfo
|
||||
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
|
||||
import org.calyxos.seedvault.core.backends.saf.SafProperties
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
|
@ -48,6 +49,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
private val notificationManager: BackupNotificationManager = mockk()
|
||||
private val backendManager: BackendManager = mockk()
|
||||
private val backend = mockk<Backend>()
|
||||
private val loader = mockk<Loader>()
|
||||
private val kv = mockk<KVRestore>()
|
||||
private val full = mockk<FullRestore>()
|
||||
private val metadataReader = mockk<MetadataReader>()
|
||||
|
@ -59,11 +61,13 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
metadataManager = metadataManager,
|
||||
notificationManager = notificationManager,
|
||||
backendManager = backendManager,
|
||||
loader = loader,
|
||||
kv = kv,
|
||||
full = full,
|
||||
metadataReader = metadataReader,
|
||||
)
|
||||
|
||||
private val restorableBackup = RestorableBackup(metadata)
|
||||
private val inputStream = mockk<InputStream>()
|
||||
private val safStorage: SafProperties = mockk()
|
||||
private val packageInfo2 = PackageInfo().apply { packageName = "org.example2" }
|
||||
|
@ -85,12 +89,22 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
|
||||
@Test
|
||||
fun `getAvailableRestoreSets() builds set from plugin response`() = runBlocking {
|
||||
val encryptedMetadata = EncryptedMetadata(token) { inputStream }
|
||||
val info1 = FileInfo(LegacyAppBackupFile.Metadata(token), 1)
|
||||
val info2 = FileInfo(LegacyAppBackupFile.Metadata(token + 1), 2)
|
||||
|
||||
coEvery { backend.getAvailableBackups() } returns sequenceOf(
|
||||
encryptedMetadata,
|
||||
EncryptedMetadata(token + 1) { inputStream }
|
||||
)
|
||||
coEvery {
|
||||
backend.list(
|
||||
topLevelFolder = null,
|
||||
AppBackupFileType.Snapshot::class, LegacyAppBackupFile.Metadata::class,
|
||||
callback = captureLambda<(FileInfo) -> Unit>()
|
||||
)
|
||||
} answers {
|
||||
val callback = lambda<(FileInfo) -> Unit>().captured
|
||||
callback(info1)
|
||||
callback(info2)
|
||||
}
|
||||
coEvery { backend.load(info1.fileHandle) } returns inputStream
|
||||
coEvery { backend.load(info2.fileHandle) } returns inputStream
|
||||
every { metadataReader.readMetadata(inputStream, token) } returns metadata
|
||||
every { metadataReader.readMetadata(inputStream, token + 1) } returns metadata
|
||||
every { inputStream.close() } just Runs
|
||||
|
@ -119,16 +133,28 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
|
||||
@Test
|
||||
fun `startRestore() returns OK`() = runBlocking {
|
||||
restore.beforeStartRestore(metadata)
|
||||
restore.beforeStartRestore(restorableBackup)
|
||||
assertEquals(TRANSPORT_OK, restore.startRestore(token, packageInfoArray))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startRestore() fetches metadata if missing`() = runBlocking {
|
||||
coEvery { backend.getAvailableBackups() } returns sequenceOf(
|
||||
EncryptedMetadata(token) { inputStream },
|
||||
EncryptedMetadata(token + 1) { inputStream }
|
||||
)
|
||||
val info1 = FileInfo(LegacyAppBackupFile.Metadata(token), 1)
|
||||
val info2 = FileInfo(LegacyAppBackupFile.Metadata(token + 1), 2)
|
||||
|
||||
coEvery {
|
||||
backend.list(
|
||||
topLevelFolder = null,
|
||||
AppBackupFileType.Snapshot::class, LegacyAppBackupFile.Metadata::class,
|
||||
callback = captureLambda<(FileInfo) -> Unit>()
|
||||
)
|
||||
} answers {
|
||||
val callback = lambda<(FileInfo) -> Unit>().captured
|
||||
callback(info1)
|
||||
callback(info2)
|
||||
}
|
||||
coEvery { backend.load(info1.fileHandle) } returns inputStream
|
||||
coEvery { backend.load(info2.fileHandle) } returns inputStream
|
||||
every { metadataReader.readMetadata(inputStream, token) } returns metadata
|
||||
every { metadataReader.readMetadata(inputStream, token + 1) } returns metadata
|
||||
every { inputStream.close() } just Runs
|
||||
|
@ -138,18 +164,28 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
|
||||
@Test
|
||||
fun `startRestore() errors if metadata is not matching token`() = runBlocking {
|
||||
coEvery { backend.getAvailableBackups() } returns sequenceOf(
|
||||
EncryptedMetadata(token + 42) { inputStream }
|
||||
)
|
||||
every { metadataReader.readMetadata(inputStream, token + 42) } returns metadata
|
||||
val otherToken = token + 42
|
||||
val info = FileInfo(LegacyAppBackupFile.Metadata(otherToken), 23)
|
||||
coEvery {
|
||||
backend.list(
|
||||
topLevelFolder = null,
|
||||
AppBackupFileType.Snapshot::class, LegacyAppBackupFile.Metadata::class,
|
||||
callback = captureLambda<(FileInfo) -> Unit>()
|
||||
)
|
||||
} answers {
|
||||
val callback = lambda<(FileInfo) -> Unit>().captured
|
||||
callback(info)
|
||||
}
|
||||
coEvery { backend.load(info.fileHandle) } returns inputStream
|
||||
every { metadataReader.readMetadata(inputStream, otherToken) } returns metadata
|
||||
every { inputStream.close() } just Runs
|
||||
|
||||
assertEquals(TRANSPORT_ERROR, restore.startRestore(token, packageInfoArray))
|
||||
assertEquals(TRANSPORT_ERROR, restore.startRestore(otherToken, packageInfoArray))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startRestore() can not be called twice`() = runBlocking {
|
||||
restore.beforeStartRestore(metadata)
|
||||
restore.beforeStartRestore(restorableBackup)
|
||||
assertEquals(TRANSPORT_OK, restore.startRestore(token, packageInfoArray))
|
||||
assertThrows(IllegalStateException::class.javaObjectType) {
|
||||
runBlocking {
|
||||
|
@ -161,13 +197,13 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
|
||||
@Test
|
||||
fun `startRestore() can be be called again after restore finished`() = runBlocking {
|
||||
restore.beforeStartRestore(metadata)
|
||||
restore.beforeStartRestore(restorableBackup)
|
||||
assertEquals(TRANSPORT_OK, restore.startRestore(token, packageInfoArray))
|
||||
|
||||
every { full.hasState() } returns false
|
||||
restore.finishRestore()
|
||||
|
||||
restore.beforeStartRestore(metadata)
|
||||
restore.beforeStartRestore(restorableBackup)
|
||||
assertEquals(TRANSPORT_OK, restore.startRestore(token, packageInfoArray))
|
||||
}
|
||||
|
||||
|
@ -201,7 +237,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
every { backendManager.backendProperties } returns safStorage
|
||||
every { safStorage.isUnavailableUsb(context) } returns false
|
||||
|
||||
restore.beforeStartRestore(metadata)
|
||||
restore.beforeStartRestore(restorableBackup)
|
||||
assertEquals(TRANSPORT_OK, restore.startRestore(token, pmPackageInfoArray))
|
||||
|
||||
verify(exactly = 0) {
|
||||
|
@ -237,7 +273,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
|
||||
@Test
|
||||
fun `nextRestorePackage() returns KV description`() = runBlocking {
|
||||
restore.beforeStartRestore(metadata)
|
||||
restore.beforeStartRestore(restorableBackup)
|
||||
restore.startRestore(token, packageInfoArray)
|
||||
|
||||
every { crypto.getNameForPackage(metadata.salt, packageName) } returns name
|
||||
|
@ -250,7 +286,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
@Test
|
||||
@Suppress("Deprecation")
|
||||
fun `v0 nextRestorePackage() returns KV description and takes precedence`() = runBlocking {
|
||||
restore.beforeStartRestore(metadata.copy(version = 0x00))
|
||||
val backup = restorableBackup.copy(backupMetadata = metadata.copy(version = 0x00))
|
||||
restore.beforeStartRestore(backup)
|
||||
restore.startRestore(token, packageInfoArray)
|
||||
|
||||
coEvery { kv.hasDataForPackage(token, packageInfo) } returns true
|
||||
|
@ -263,7 +300,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
@Test
|
||||
@Suppress("deprecation")
|
||||
fun `v0 nextRestorePackage() returns full description if no KV data found`() = runBlocking {
|
||||
restore.beforeStartRestore(metadata.copy(version = 0x00))
|
||||
val backup = restorableBackup.copy(backupMetadata = metadata.copy(version = 0x00))
|
||||
restore.beforeStartRestore(backup)
|
||||
restore.startRestore(token, packageInfoArray)
|
||||
|
||||
coEvery { kv.hasDataForPackage(token, packageInfo) } returns false
|
||||
|
@ -278,7 +316,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
fun `nextRestorePackage() tries next package if one has no backup type()`() = runBlocking {
|
||||
metadata.packageMetadataMap[packageName] =
|
||||
metadata.packageMetadataMap[packageName]!!.copy(backupType = null)
|
||||
restore.beforeStartRestore(metadata)
|
||||
restore.beforeStartRestore(restorableBackup)
|
||||
restore.startRestore(token, packageInfoArray2)
|
||||
|
||||
every { crypto.getNameForPackage(metadata.salt, packageInfo2.packageName) } returns name2
|
||||
|
@ -292,7 +330,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
|
||||
@Test
|
||||
fun `nextRestorePackage() returns all packages from startRestore()`() = runBlocking {
|
||||
restore.beforeStartRestore(metadata)
|
||||
restore.beforeStartRestore(restorableBackup)
|
||||
restore.startRestore(token, packageInfoArray2)
|
||||
|
||||
every { crypto.getNameForPackage(metadata.salt, packageName) } returns name
|
||||
|
@ -314,7 +352,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
@Test
|
||||
@Suppress("deprecation")
|
||||
fun `v0 nextRestorePackage() returns all packages from startRestore()`() = runBlocking {
|
||||
restore.beforeStartRestore(metadata.copy(version = 0x00))
|
||||
val backup = restorableBackup.copy(backupMetadata = metadata.copy(version = 0x00))
|
||||
restore.beforeStartRestore(backup)
|
||||
restore.startRestore(token, packageInfoArray2)
|
||||
|
||||
coEvery { kv.hasDataForPackage(token, packageInfo) } returns true
|
||||
|
@ -336,7 +375,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
@Test
|
||||
@Suppress("Deprecation")
|
||||
fun `v0 when kv#hasDataForPackage() throws, it tries next package`() = runBlocking {
|
||||
restore.beforeStartRestore(metadata.copy(version = 0x00))
|
||||
val backup = restorableBackup.copy(backupMetadata = metadata.copy(version = 0x00))
|
||||
restore.beforeStartRestore(backup)
|
||||
restore.startRestore(token, packageInfoArray)
|
||||
|
||||
coEvery { kv.hasDataForPackage(token, packageInfo) } throws IOException()
|
||||
|
@ -347,7 +387,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
@Test
|
||||
@Suppress("deprecation")
|
||||
fun `v0 when full#hasDataForPackage() throws, it tries next package`() = runBlocking {
|
||||
restore.beforeStartRestore(metadata.copy(version = 0x00))
|
||||
val backup = restorableBackup.copy(backupMetadata = metadata.copy(version = 0x00))
|
||||
restore.beforeStartRestore(backup)
|
||||
restore.startRestore(token, packageInfoArray)
|
||||
|
||||
coEvery { kv.hasDataForPackage(token, packageInfo) } returns false
|
||||
|
|
|
@ -55,6 +55,7 @@ internal class RestoreV0IntegrationTest : TransportTest() {
|
|||
private val metadataReader = MetadataReaderImpl(cryptoImpl)
|
||||
private val notificationManager = mockk<BackupNotificationManager>()
|
||||
private val backendManager: BackendManager = mockk()
|
||||
private val loader = mockk<Loader>()
|
||||
|
||||
@Suppress("Deprecation")
|
||||
private val legacyPlugin = mockk<LegacyStoragePlugin>()
|
||||
|
@ -76,10 +77,14 @@ internal class RestoreV0IntegrationTest : TransportTest() {
|
|||
metadataManager = metadataManager,
|
||||
notificationManager = notificationManager,
|
||||
backendManager = backendManager,
|
||||
loader = loader,
|
||||
kv = kvRestore,
|
||||
full = fullRestore,
|
||||
metadataReader = metadataReader,
|
||||
).apply { beforeStartRestore(metadata.copy(version = 0x00)) }
|
||||
).apply {
|
||||
val backup = RestorableBackup(metadata.copy(version = 0x00))
|
||||
beforeStartRestore(backup)
|
||||
}
|
||||
|
||||
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
|
||||
private val appData = ("562AB665C3543120FC794D7CDA3AC18E5959235A4D" +
|
||||
|
|
Loading…
Reference in a new issue