Request @pm@ backup after initialization
to avoid a 2nd restore set being used. This also changes the initialization behavior to only create the restore set folder and upload the metadata only when we actually need to. This way, double inits are not creating new restore sets on the backup destination.
This commit is contained in:
parent
23787a373e
commit
2da989971b
11 changed files with 118 additions and 87 deletions
|
@ -47,7 +47,7 @@ class KoinInstrumentationTestApp : App() {
|
||||||
|
|
||||||
viewModel {
|
viewModel {
|
||||||
currentBackupStorageViewModel =
|
currentBackupStorageViewModel =
|
||||||
spyk(BackupStorageViewModel(context, get(), get(), get()))
|
spyk(BackupStorageViewModel(context, get(), get(), get(), get()))
|
||||||
currentBackupStorageViewModel!!
|
currentBackupStorageViewModel!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ open class App : Application() {
|
||||||
|
|
||||||
viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
||||||
viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
||||||
viewModel { BackupStorageViewModel(this@App, get(), get(), get()) }
|
viewModel { BackupStorageViewModel(this@App, get(), get(), get(), get()) }
|
||||||
viewModel { RestoreStorageViewModel(this@App, get(), get()) }
|
viewModel { RestoreStorageViewModel(this@App, get(), get()) }
|
||||||
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
||||||
viewModel { FileSelectionViewModel(this@App, get()) }
|
viewModel { FileSelectionViewModel(this@App, get()) }
|
||||||
|
|
|
@ -59,14 +59,15 @@ internal class MetadataManager(
|
||||||
/**
|
/**
|
||||||
* Call this when initializing a new device.
|
* Call this when initializing a new device.
|
||||||
*
|
*
|
||||||
* Existing [BackupMetadata] will be cleared, use the given new token,
|
* Existing [BackupMetadata] will be cleared
|
||||||
* and written encrypted to the given [OutputStream] as well as the internal cache.
|
* and new metadata with the given [token] will be written to the internal cache
|
||||||
|
* with a fresh salt.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun onDeviceInitialization(token: Long, metadataOutputStream: OutputStream) {
|
fun onDeviceInitialization(token: Long) {
|
||||||
val salt = crypto.getRandomBytes(METADATA_SALT_SIZE).encodeBase64()
|
val salt = crypto.getRandomBytes(METADATA_SALT_SIZE).encodeBase64()
|
||||||
modifyMetadata(metadataOutputStream) {
|
modifyCachedMetadata {
|
||||||
metadata = BackupMetadata(token = token, salt = salt)
|
metadata = BackupMetadata(token = token, salt = salt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,22 +35,13 @@ internal class DocumentsProviderStoragePlugin(
|
||||||
override suspend fun startNewRestoreSet(token: Long) {
|
override suspend fun startNewRestoreSet(token: Long) {
|
||||||
// reset current storage
|
// reset current storage
|
||||||
storage.reset(token)
|
storage.reset(token)
|
||||||
|
|
||||||
// get or create root backup dir
|
|
||||||
storage.rootBackupDir ?: throw IOException()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override suspend fun initializeDevice() {
|
override suspend fun initializeDevice() {
|
||||||
// wipe existing data
|
|
||||||
storage.getSetDir()?.deleteContents(context)
|
|
||||||
|
|
||||||
// reset storage without new token, so folders get recreated
|
// reset storage without new token, so folders get recreated
|
||||||
// otherwise stale DocumentFiles will hang around
|
// otherwise stale DocumentFiles will hang around
|
||||||
storage.reset(null)
|
storage.reset(null)
|
||||||
|
|
||||||
// create backup folders
|
|
||||||
storage.currentSetDir ?: throw IOException()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
|
|
|
@ -86,12 +86,13 @@ internal class BackupCoordinator(
|
||||||
* @return the token of the new [RestoreSet].
|
* @return the token of the new [RestoreSet].
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private suspend fun startNewRestoreSet(): Long {
|
private suspend fun startNewRestoreSet() {
|
||||||
val token = clock.time()
|
val token = clock.time()
|
||||||
Log.i(TAG, "Starting new RestoreSet with token $token...")
|
Log.i(TAG, "Starting new RestoreSet with token $token...")
|
||||||
settingsManager.setNewToken(token)
|
settingsManager.setNewToken(token)
|
||||||
plugin.startNewRestoreSet(token)
|
plugin.startNewRestoreSet(token)
|
||||||
return token
|
Log.d(TAG, "Resetting backup metadata...")
|
||||||
|
metadataManager.onDeviceInitialization(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -115,18 +116,14 @@ internal class BackupCoordinator(
|
||||||
suspend fun initializeDevice(): Int = try {
|
suspend fun initializeDevice(): Int = try {
|
||||||
// we don't respect the intended system behavior here by always starting a new [RestoreSet]
|
// we don't respect the intended system behavior here by always starting a new [RestoreSet]
|
||||||
// instead of simply deleting the current one
|
// instead of simply deleting the current one
|
||||||
val token = startNewRestoreSet()
|
startNewRestoreSet()
|
||||||
Log.i(TAG, "Initialize Device!")
|
Log.i(TAG, "Initialize Device!")
|
||||||
plugin.initializeDevice()
|
plugin.initializeDevice()
|
||||||
Log.d(TAG, "Resetting backup metadata for token $token...")
|
|
||||||
plugin.getMetadataOutputStream(token).use {
|
|
||||||
metadataManager.onDeviceInitialization(token, it)
|
|
||||||
}
|
|
||||||
// [finishBackup] will only be called when we return [TRANSPORT_OK] here
|
// [finishBackup] will only be called when we return [TRANSPORT_OK] here
|
||||||
// so we remember that we initialized successfully
|
// so we remember that we initialized successfully
|
||||||
state.calledInitialize = true
|
state.calledInitialize = true
|
||||||
TRANSPORT_OK
|
TRANSPORT_OK
|
||||||
} catch (e: IOException) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error initializing device", e)
|
Log.e(TAG, "Error initializing device", e)
|
||||||
// Show error notification if we needed init or were ready for backups
|
// Show error notification if we needed init or were ready for backups
|
||||||
if (metadataManager.requiresInit || settingsManager.canDoBackupNow()) nm.onBackupError()
|
if (metadataManager.requiresInit || settingsManager.canDoBackupNow()) nm.onBackupError()
|
||||||
|
@ -222,14 +219,11 @@ internal class BackupCoordinator(
|
||||||
state.cancelReason = UNKNOWN_ERROR
|
state.cancelReason = UNKNOWN_ERROR
|
||||||
if (metadataManager.requiresInit) {
|
if (metadataManager.requiresInit) {
|
||||||
Log.w(TAG, "Metadata requires re-init!")
|
Log.w(TAG, "Metadata requires re-init!")
|
||||||
// start a new restore set to upgrade from legacy format
|
// Tell the system that we are not initialized, it will initialize us afterwards.
|
||||||
// by starting a clean backup with all files using the new version
|
// This will start a new restore set to upgrade from legacy format
|
||||||
try {
|
// by starting a clean backup with all files using the new version.
|
||||||
startNewRestoreSet()
|
//
|
||||||
} catch (e: IOException) {
|
// This causes a backup error, but things should go back to normal afterwards.
|
||||||
Log.e(TAG, "Error starting new restore set", e)
|
|
||||||
}
|
|
||||||
// this causes a backup error, but things should go back to normal afterwards
|
|
||||||
return TRANSPORT_NOT_INITIALIZED
|
return TRANSPORT_NOT_INITIALIZED
|
||||||
}
|
}
|
||||||
val token = settingsManager.getToken() ?: error("no token in performFullBackup")
|
val token = settingsManager.getToken() ?: error("no token in performFullBackup")
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.stevesoltys.seedvault.transport.backup
|
||||||
|
|
||||||
|
import android.app.backup.BackupProgress
|
||||||
|
import android.app.backup.IBackupManager
|
||||||
|
import android.app.backup.IBackupObserver
|
||||||
|
import android.os.UserHandle
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import com.stevesoltys.seedvault.BackupMonitor
|
||||||
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
|
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
|
||||||
|
|
||||||
|
class BackupInitializer(
|
||||||
|
private val backupManager: IBackupManager,
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = BackupInitializer::class.simpleName
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initialize(onError: () -> Unit, onSuccess: () -> Unit) {
|
||||||
|
val observer = BackupObserver("Initialization", onError) {
|
||||||
|
// After successful initialization, we request a @pm@ backup right away,
|
||||||
|
// because if this finds empty state, it asks us to do another initialization.
|
||||||
|
// And then we end up with yet another restore set token.
|
||||||
|
// Since we want the final token as soon as possible, we need to get it here.
|
||||||
|
Log.d(TAG, "Requesting initial $MAGIC_PACKAGE_MANAGER backup...")
|
||||||
|
backupManager.requestBackup(
|
||||||
|
arrayOf(MAGIC_PACKAGE_MANAGER),
|
||||||
|
BackupObserver("Initial backup of @pm@", onError, onSuccess),
|
||||||
|
BackupMonitor(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
backupManager.initializeTransportsForUser(
|
||||||
|
UserHandle.myUserId(),
|
||||||
|
arrayOf(TRANSPORT_ID),
|
||||||
|
observer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private inner class BackupObserver(
|
||||||
|
private val operation: String,
|
||||||
|
private val onError: () -> Unit,
|
||||||
|
private val onSuccess: () -> Unit,
|
||||||
|
) : IBackupObserver.Stub() {
|
||||||
|
override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResult(target: String, status: Int) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun backupFinished(status: Int) {
|
||||||
|
if (Log.isLoggable(TAG, Log.INFO)) {
|
||||||
|
Log.i(TAG, "$operation finished. Status: $status")
|
||||||
|
}
|
||||||
|
if (status == 0) {
|
||||||
|
onSuccess()
|
||||||
|
} else {
|
||||||
|
onError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val backupModule = module {
|
val backupModule = module {
|
||||||
|
single { BackupInitializer(get()) }
|
||||||
single { InputFactory() }
|
single { InputFactory() }
|
||||||
single {
|
single {
|
||||||
PackageService(
|
PackageService(
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.stevesoltys.seedvault.ui.recoverycode
|
package com.stevesoltys.seedvault.ui.recoverycode
|
||||||
|
|
||||||
import android.app.backup.IBackupManager
|
import android.app.backup.IBackupManager
|
||||||
import android.os.UserHandle
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import cash.z.ecc.android.bip39.Mnemonics
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
|
@ -12,8 +11,7 @@ import cash.z.ecc.android.bip39.toSeed
|
||||||
import com.stevesoltys.seedvault.App
|
import com.stevesoltys.seedvault.App
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.crypto.Crypto
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
|
import com.stevesoltys.seedvault.transport.backup.BackupInitializer
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
|
||||||
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.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
|
@ -32,7 +30,7 @@ internal class RecoveryCodeViewModel(
|
||||||
private val crypto: Crypto,
|
private val crypto: Crypto,
|
||||||
private val keyManager: KeyManager,
|
private val keyManager: KeyManager,
|
||||||
private val backupManager: IBackupManager,
|
private val backupManager: IBackupManager,
|
||||||
private val backupCoordinator: BackupCoordinator,
|
private val backupInitializer: BackupInitializer,
|
||||||
private val notificationManager: BackupNotificationManager,
|
private val notificationManager: BackupNotificationManager,
|
||||||
private val storageBackup: StorageBackup,
|
private val storageBackup: StorageBackup,
|
||||||
) : AndroidViewModel(app) {
|
) : AndroidViewModel(app) {
|
||||||
|
@ -109,11 +107,9 @@ internal class RecoveryCodeViewModel(
|
||||||
storageBackup.clearCache()
|
storageBackup.clearCache()
|
||||||
try {
|
try {
|
||||||
// initialize the new location
|
// initialize the new location
|
||||||
if (backupManager.isBackupEnabled) backupManager.initializeTransportsForUser(
|
if (backupManager.isBackupEnabled) backupInitializer.initialize({ }) {
|
||||||
UserHandle.myUserId(),
|
// no-op
|
||||||
arrayOf(TRANSPORT_ID),
|
}
|
||||||
null
|
|
||||||
)
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error starting new RestoreSet", e)
|
Log.e(TAG, "Error starting new RestoreSet", e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,16 @@
|
||||||
package com.stevesoltys.seedvault.ui.storage
|
package com.stevesoltys.seedvault.ui.storage
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.backup.BackupProgress
|
|
||||||
import android.app.backup.IBackupManager
|
import android.app.backup.IBackupManager
|
||||||
import android.app.backup.IBackupObserver
|
|
||||||
import android.app.job.JobInfo
|
import android.app.job.JobInfo
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.UserHandle
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.WorkerThread
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.storage.StorageBackupJobService
|
import com.stevesoltys.seedvault.storage.StorageBackupJobService
|
||||||
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
|
import com.stevesoltys.seedvault.transport.backup.BackupInitializer
|
||||||
import com.stevesoltys.seedvault.worker.AppBackupWorker
|
import com.stevesoltys.seedvault.worker.AppBackupWorker
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -28,6 +24,7 @@ private val TAG = BackupStorageViewModel::class.java.simpleName
|
||||||
internal class BackupStorageViewModel(
|
internal class BackupStorageViewModel(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val backupManager: IBackupManager,
|
private val backupManager: IBackupManager,
|
||||||
|
private val backupInitializer: BackupInitializer,
|
||||||
private val storageBackup: StorageBackup,
|
private val storageBackup: StorageBackup,
|
||||||
settingsManager: SettingsManager,
|
settingsManager: SettingsManager,
|
||||||
) : StorageViewModel(app, settingsManager) {
|
) : StorageViewModel(app, settingsManager) {
|
||||||
|
@ -52,13 +49,23 @@ internal class BackupStorageViewModel(
|
||||||
storageBackup.clearCache()
|
storageBackup.clearCache()
|
||||||
try {
|
try {
|
||||||
// initialize the new location (if backups are enabled)
|
// initialize the new location (if backups are enabled)
|
||||||
if (backupManager.isBackupEnabled) backupManager.initializeTransportsForUser(
|
if (backupManager.isBackupEnabled) {
|
||||||
UserHandle.myUserId(),
|
val onError = {
|
||||||
arrayOf(TRANSPORT_ID),
|
Log.e(TAG, "Error starting new RestoreSet")
|
||||||
// if storage is on USB and this is not SetupWizard, do a backup right away
|
onInitializationError()
|
||||||
InitializationObserver(isUsb && !isSetupWizard)
|
}
|
||||||
) else {
|
backupInitializer.initialize(onError) {
|
||||||
InitializationObserver(false).backupFinished(0)
|
val requestBackup = isUsb && !isSetupWizard
|
||||||
|
if (requestBackup) {
|
||||||
|
Log.i(TAG, "Requesting a backup now, because we use USB storage")
|
||||||
|
AppBackupWorker.scheduleNow(app, reschedule = false)
|
||||||
|
}
|
||||||
|
// notify the UI that the location has been set
|
||||||
|
mLocationChecked.postEvent(LocationResult())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// notify the UI that the location has been set
|
||||||
|
mLocationChecked.postEvent(LocationResult())
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error starting new RestoreSet", e)
|
Log.e(TAG, "Error starting new RestoreSet", e)
|
||||||
|
@ -90,35 +97,6 @@ internal class BackupStorageViewModel(
|
||||||
BackupJobService.cancelJob(app)
|
BackupJobService.cancelJob(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
private inner class InitializationObserver(val requestBackup: Boolean) :
|
|
||||||
IBackupObserver.Stub() {
|
|
||||||
override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResult(target: String, status: Int) {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun backupFinished(status: Int) {
|
|
||||||
if (Log.isLoggable(TAG, Log.INFO)) {
|
|
||||||
Log.i(TAG, "Initialization finished. Status: $status")
|
|
||||||
}
|
|
||||||
if (status == 0) {
|
|
||||||
// notify the UI that the location has been set
|
|
||||||
mLocationChecked.postEvent(LocationResult())
|
|
||||||
if (requestBackup) {
|
|
||||||
val isUsb = settingsManager.getStorage()?.isUsb ?: false
|
|
||||||
AppBackupWorker.scheduleNow(app, reschedule = !isUsb)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// notify the UI that the location was invalid
|
|
||||||
onInitializationError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onInitializationError() {
|
private fun onInitializationError() {
|
||||||
val errorMsg = app.getString(R.string.storage_check_fragment_backup_error)
|
val errorMsg = app.getString(R.string.storage_check_fragment_backup_error)
|
||||||
mLocationChecked.postEvent(LocationResult(errorMsg))
|
mLocationChecked.postEvent(LocationResult(errorMsg))
|
||||||
|
|
|
@ -100,7 +100,7 @@ class MetadataManagerTest {
|
||||||
expectReadFromCache()
|
expectReadFromCache()
|
||||||
expectModifyMetadata(initialMetadata)
|
expectModifyMetadata(initialMetadata)
|
||||||
|
|
||||||
manager.onDeviceInitialization(token, storageOutputStream)
|
manager.onDeviceInitialization(token)
|
||||||
|
|
||||||
assertEquals(token, manager.getBackupToken())
|
assertEquals(token, manager.getBackupToken())
|
||||||
assertEquals(0L, manager.getLastBackupTime())
|
assertEquals(0L, manager.getLastBackupTime())
|
||||||
|
|
|
@ -69,22 +69,18 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
fun `device initialization succeeds and delegates to plugin`() = runBlocking {
|
fun `device initialization succeeds and delegates to plugin`() = runBlocking {
|
||||||
expectStartNewRestoreSet()
|
expectStartNewRestoreSet()
|
||||||
coEvery { plugin.initializeDevice() } just Runs
|
coEvery { plugin.initializeDevice() } just Runs
|
||||||
coEvery { plugin.getOutputStream(token, FILE_BACKUP_METADATA) } returns metadataOutputStream
|
|
||||||
every { metadataManager.onDeviceInitialization(token, metadataOutputStream) } just Runs
|
|
||||||
every { kv.hasState() } returns false
|
every { kv.hasState() } returns false
|
||||||
every { full.hasState() } returns false
|
every { full.hasState() } returns false
|
||||||
every { metadataOutputStream.close() } just Runs
|
|
||||||
|
|
||||||
assertEquals(TRANSPORT_OK, backup.initializeDevice())
|
assertEquals(TRANSPORT_OK, backup.initializeDevice())
|
||||||
assertEquals(TRANSPORT_OK, backup.finishBackup())
|
assertEquals(TRANSPORT_OK, backup.finishBackup())
|
||||||
|
|
||||||
verify { metadataOutputStream.close() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun expectStartNewRestoreSet() {
|
private suspend fun expectStartNewRestoreSet() {
|
||||||
every { clock.time() } returns token
|
every { clock.time() } returns token
|
||||||
every { settingsManager.setNewToken(token) } just Runs
|
every { settingsManager.setNewToken(token) } just Runs
|
||||||
coEvery { plugin.startNewRestoreSet(token) } just Runs
|
coEvery { plugin.startNewRestoreSet(token) } just Runs
|
||||||
|
every { metadataManager.onDeviceInitialization(token) } just Runs
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -136,6 +132,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
every { clock.time() } returns token + 1
|
every { clock.time() } returns token + 1
|
||||||
every { settingsManager.setNewToken(token + 1) } just Runs
|
every { settingsManager.setNewToken(token + 1) } just Runs
|
||||||
coEvery { plugin.startNewRestoreSet(token + 1) } just Runs
|
coEvery { plugin.startNewRestoreSet(token + 1) } just Runs
|
||||||
|
every { metadataManager.onDeviceInitialization(token + 1) } just Runs
|
||||||
|
|
||||||
every { data.close() } just Runs
|
every { data.close() } just Runs
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue