From b9cac5ea87fd9a69986d3cfd0e7380609bded245 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 18 Dec 2019 13:14:25 -0300 Subject: [PATCH] Introduce MetadataManager to handle all metadata related to backups This now updates the metadata on remote storage and internal cache after each successful package backup. --- app/build.gradle | 12 +- .../seedvault/DocumentsStorageTest.kt | 6 +- .../java/com/stevesoltys/seedvault/App.kt | 5 +- .../seedvault/BackupNotificationManager.kt | 7 +- .../java/com/stevesoltys/seedvault/Clock.kt | 13 ++ .../seedvault/UsbIntentReceiver.kt | 12 +- .../seedvault/metadata/Metadata.kt | 6 +- .../seedvault/metadata/MetadataManager.kt | 119 +++++++++++++++++ .../seedvault/metadata/MetadataModule.kt | 2 + .../seedvault/metadata/MetadataReader.kt | 11 +- .../seedvault/metadata/MetadataWriter.kt | 13 +- .../plugins/saf/DocumentsProviderModule.kt | 2 +- .../seedvault/plugins/saf/DocumentsStorage.kt | 10 +- .../seedvault/settings/SettingsFragment.kt | 13 +- .../seedvault/settings/SettingsManager.kt | 46 ------- .../seedvault/settings/SettingsViewModel.kt | 15 ++- .../transport/backup/BackupCoordinator.kt | 30 +++-- .../transport/restore/RestoreCoordinator.kt | 6 +- .../ui/storage/BackupStorageViewModel.kt | 5 +- .../seedvault/ui/storage/StorageViewModel.kt | 3 - .../seedvault/metadata/MetadataManagerTest.kt | 124 ++++++++++++++++++ .../seedvault/metadata/MetadataReaderTest.kt | 2 +- .../metadata/MetadataWriterDecoderTest.kt | 2 +- .../transport/CoordinatorIntegrationTest.kt | 13 +- .../seedvault/transport/TransportTest.kt | 2 + .../transport/backup/BackupCoordinatorTest.kt | 13 +- .../restore/RestoreCoordinatorTest.kt | 4 +- 27 files changed, 361 insertions(+), 135 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/seedvault/Clock.kt create mode 100644 app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt create mode 100644 app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt diff --git a/app/build.gradle b/app/build.gradle index 01efa568..82fb5174 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,6 +13,7 @@ android { minSdkVersion 29 targetSdkVersion 29 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments disableAnalytics: 'true' } buildTypes { @@ -35,6 +36,9 @@ android { events "passed", "skipped", "failed" } } + unitTests { + includeAndroidResources = true + } } sourceSets { @@ -119,10 +123,14 @@ dependencies { lintChecks 'com.github.thirdegg:lint-rules:0.0.4-alpha' + def junit_version = "5.5.2" testImplementation aospDeps - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2' + testImplementation 'androidx.test.ext:junit:1.1.1' + testImplementation 'org.robolectric:robolectric:4.3.1' + testImplementation "org.junit.jupiter:junit-jupiter-api:$junit_version" testImplementation 'io.mockk:mockk:1.9.3' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2' + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit_version" + testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$junit_version" androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0' diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/DocumentsStorageTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/DocumentsStorageTest.kt index 069e48b0..3b673dbb 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/DocumentsStorageTest.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/DocumentsStorageTest.kt @@ -3,9 +3,10 @@ package com.stevesoltys.seedvault import androidx.documentfile.provider.DocumentFile import androidx.test.platform.app.InstrumentationRegistry import androidx.test.runner.AndroidJUnit4 -import com.stevesoltys.seedvault.settings.SettingsManager +import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage import com.stevesoltys.seedvault.plugins.saf.createOrGetFile +import com.stevesoltys.seedvault.settings.SettingsManager import org.junit.After import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertNotNull @@ -22,8 +23,9 @@ private const val filename = "test-file" class DocumentsStorageTest : KoinComponent { private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val metadataManager by inject() private val settingsManager by inject() - private val storage = DocumentsStorage(context, settingsManager) + private val storage = DocumentsStorage(context, metadataManager, settingsManager) private lateinit var file: DocumentFile diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index bfc44ca6..44636ff2 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -9,11 +9,11 @@ import android.os.ServiceManager.getService import com.stevesoltys.seedvault.crypto.cryptoModule import com.stevesoltys.seedvault.header.headerModule import com.stevesoltys.seedvault.metadata.metadataModule +import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule import com.stevesoltys.seedvault.restore.RestoreViewModel import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsViewModel import com.stevesoltys.seedvault.transport.backup.backupModule -import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule import com.stevesoltys.seedvault.transport.restore.restoreModule import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeViewModel import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel @@ -33,9 +33,10 @@ class App : Application() { private val appModule = module { single { SettingsManager(this@App) } single { BackupNotificationManager(this@App) } + single { Clock() } factory { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) } - viewModel { SettingsViewModel(this@App, get(), get()) } + viewModel { SettingsViewModel(this@App, get(), get(), get()) } viewModel { RecoveryCodeViewModel(this@App, get()) } viewModel { BackupStorageViewModel(this@App, get(), get()) } viewModel { RestoreStorageViewModel(this@App, get(), get()) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/BackupNotificationManager.kt b/app/src/main/java/com/stevesoltys/seedvault/BackupNotificationManager.kt index f82a8fa9..4d8645b1 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/BackupNotificationManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/BackupNotificationManager.kt @@ -10,7 +10,6 @@ import android.content.Context import android.content.Intent import androidx.core.app.NotificationCompat.* import com.stevesoltys.seedvault.settings.SettingsActivity -import java.util.* private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver" private const val CHANNEL_ID_ERROR = "NotificationError" @@ -48,7 +47,7 @@ class BackupNotificationManager(private val context: Context) { val notification = observerBuilder.apply { setContentTitle(context.getString(R.string.notification_title)) setContentText(app) - setWhen(Date().time) + setWhen(System.currentTimeMillis()) setProgress(expected, transferred, false) priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW }.build() @@ -64,7 +63,7 @@ class BackupNotificationManager(private val context: Context) { val notification = observerBuilder.apply { setContentTitle(title) setContentText(app) - setWhen(Date().time) + setWhen(System.currentTimeMillis()) priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW }.build() nm.notify(NOTIFICATION_ID_OBSERVER, notification) @@ -82,7 +81,7 @@ class BackupNotificationManager(private val context: Context) { val notification = errorBuilder.apply { setContentTitle(context.getString(R.string.notification_error_title)) setContentText(context.getString(R.string.notification_error_text)) - setWhen(Date().time) + setWhen(System.currentTimeMillis()) setOnlyAlertOnce(true) setAutoCancel(true) mActions = arrayListOf(action) diff --git a/app/src/main/java/com/stevesoltys/seedvault/Clock.kt b/app/src/main/java/com/stevesoltys/seedvault/Clock.kt new file mode 100644 index 00000000..3a39ffe1 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/Clock.kt @@ -0,0 +1,13 @@ +package com.stevesoltys.seedvault + +/** + * This class only exists, so we can mock the time in tests. + */ +class Clock { + /** + * Returns the current time in milliseconds (Unix time). + */ + fun time(): Long { + return System.currentTimeMillis() + } +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt b/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt index 7ca3ab0c..4ec749e9 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt @@ -11,20 +11,20 @@ import android.net.Uri import android.os.Handler import android.provider.DocumentsContract import android.util.Log +import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.settings.FlashDrive import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.transport.requestBackup import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE -import org.koin.core.KoinComponent -import org.koin.core.inject -import java.util.* +import org.koin.core.context.GlobalContext.get import java.util.concurrent.TimeUnit.HOURS private val TAG = UsbIntentReceiver::class.java.simpleName -class UsbIntentReceiver : UsbMonitor(), KoinComponent { +class UsbIntentReceiver : UsbMonitor() { - private val settingsManager by inject() + private val settingsManager: SettingsManager by lazy { get().koin.get() } + private val metadataManager: MetadataManager by lazy { get().koin.get() } override fun shouldMonitorStatus(context: Context, action: String, device: UsbDevice): Boolean { if (action != ACTION_USB_DEVICE_ATTACHED) return false @@ -33,7 +33,7 @@ class UsbIntentReceiver : UsbMonitor(), KoinComponent { val attachedFlashDrive = FlashDrive.from(device) return if (savedFlashDrive == attachedFlashDrive) { Log.d(TAG, "Matches stored device, checking backup time...") - if (Date().time - settingsManager.getBackupTime() >= HOURS.toMillis(24)) { + if (System.currentTimeMillis() - metadataManager.getLastBackupTime() >= HOURS.toMillis(24)) { Log.d(TAG, "Last backup older than 24 hours, requesting a backup...") true } else { diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt index 5a9321ca..4d5c0b32 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt @@ -7,11 +7,11 @@ import java.io.InputStream data class BackupMetadata( internal val version: Byte = VERSION, internal val token: Long, - internal val time: Long = System.currentTimeMillis(), + internal var time: Long = 0L, internal val androidVersion: Int = Build.VERSION.SDK_INT, internal val androidIncremental: String = Build.VERSION.INCREMENTAL, internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}", - internal val packageMetadata: Map = HashMap() + internal val packageMetadata: HashMap = HashMap() ) internal const val JSON_METADATA = "@meta@" @@ -23,7 +23,7 @@ internal const val JSON_METADATA_INCREMENTAL = "incremental" internal const val JSON_METADATA_NAME = "name" data class PackageMetadata( - internal val time: Long, + internal var time: Long, internal val version: Long? = null, internal val installer: String? = null, internal val signatures: List? = null diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt new file mode 100644 index 00000000..0f396d42 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt @@ -0,0 +1,119 @@ +package com.stevesoltys.seedvault.metadata + +import android.content.Context +import android.content.Context.MODE_PRIVATE +import android.util.Log +import androidx.annotation.VisibleForTesting +import androidx.annotation.WorkerThread +import com.stevesoltys.seedvault.Clock +import java.io.FileNotFoundException +import java.io.IOException +import java.io.OutputStream + +private val TAG = MetadataManager::class.java.simpleName +@VisibleForTesting +internal const val METADATA_CACHE_FILE = "metadata.cache" + +@WorkerThread +class MetadataManager( + private val context: Context, + private val clock: Clock, + private val metadataWriter: MetadataWriter, + private val metadataReader: MetadataReader) { + + private val uninitializedMetadata = BackupMetadata(token = 0L) + private var metadata: BackupMetadata = uninitializedMetadata + get() { + if (field == uninitializedMetadata) { + field = try { + getMetadataFromCache() ?: throw IOException() + } catch (e: IOException) { + // create new default metadata + // Attention: If this happens due to a read error, we will overwrite remote metadata + Log.w(TAG, "Creating new metadata...") + BackupMetadata(token = clock.time()) + } + } + return field + } + + /** + * Call this when initializing a new device. + * + * A new backup token will be generated. + * Existing [BackupMetadata] will be cleared + * and written encrypted to the given [OutputStream] as well as the internal cache. + */ + @Synchronized + @Throws(IOException::class) + fun onDeviceInitialization(metadataOutputStream: OutputStream) { + metadata = BackupMetadata(token = clock.time()) + metadataWriter.write(metadata, metadataOutputStream) + writeMetadataToCache() + } + + /** + * Call this after a package has been backed up successfully. + * + * It updates the packages' metadata + * and writes it encrypted to the given [OutputStream] as well as the internal cache. + */ + @Synchronized + @Throws(IOException::class) + fun onPackageBackedUp(packageName: String, metadataOutputStream: OutputStream) { + val oldMetadata = metadata.copy() + val now = clock.time() + metadata.time = now + if (metadata.packageMetadata.containsKey(packageName)) { + metadata.packageMetadata[packageName]?.time = now + } else { + metadata.packageMetadata[packageName] = PackageMetadata(time = now) + } + try { + metadataWriter.write(metadata, metadataOutputStream) + } catch (e: IOException) { + Log.w(TAG, "Error writing metadata to storage", e) + // revert metadata and do not write it to cache + metadata = oldMetadata + throw IOException(e) + } + writeMetadataToCache() + } + + @Synchronized + fun getBackupToken(): Long = metadata.token + + /** + * Returns the last backup time in unix epoch milli seconds. + * + * Note that this might be a blocking I/O call. + */ + @Synchronized + fun getLastBackupTime(): Long = metadata.time + + @Synchronized + @VisibleForTesting + private fun getMetadataFromCache(): BackupMetadata? { + try { + with(context.openFileInput(METADATA_CACHE_FILE)) { + return metadataReader.decode(readBytes()) + } + } catch (e: SecurityException) { + Log.e(TAG, "Error parsing cached metadata", e) + return null + } catch (e: FileNotFoundException) { + Log.d(TAG, "Cached metadata not found, creating...") + return null + } + } + + @Synchronized + @VisibleForTesting + @Throws(IOException::class) + private fun writeMetadataToCache() { + with(context.openFileOutput(METADATA_CACHE_FILE, MODE_PRIVATE)) { + write(metadataWriter.encode(metadata)) + } + } + +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt index 2c03a999..1ba64b33 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt @@ -1,8 +1,10 @@ package com.stevesoltys.seedvault.metadata +import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val metadataModule = module { + single { MetadataManager(androidContext(), get(), get(), get()) } single { MetadataWriterImpl(get()) } single { MetadataReaderImpl(get()) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt index 2f53449e..29b6472a 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt @@ -1,6 +1,5 @@ package com.stevesoltys.seedvault.metadata -import androidx.annotation.VisibleForTesting import com.stevesoltys.seedvault.Utf8 import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.header.UnsupportedVersionException @@ -16,6 +15,9 @@ interface MetadataReader { @Throws(SecurityException::class, DecryptionFailedException::class, UnsupportedVersionException::class, IOException::class) fun readMetadata(inputStream: InputStream, expectedToken: Long): BackupMetadata + @Throws(SecurityException::class) + fun decode(bytes: ByteArray, expectedVersion: Byte? = null, expectedToken: Long? = null): BackupMetadata + } internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader { @@ -33,9 +35,8 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader { return decode(metadataBytes, version, expectedToken) } - @VisibleForTesting @Throws(SecurityException::class) - internal fun decode(bytes: ByteArray, expectedVersion: Byte, expectedToken: Long): BackupMetadata { + override fun decode(bytes: ByteArray, expectedVersion: Byte?, expectedToken: Long?): BackupMetadata { // NOTE: We don't do extensive validation of the parsed input here, // because it was encrypted with authentication, so we should be able to trust it. // @@ -46,11 +47,11 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader { // get backup metadata and check expectations val meta = json.getJSONObject(JSON_METADATA) val version = meta.getInt(JSON_METADATA_VERSION).toByte() - if (version != expectedVersion) { + if (expectedVersion != null && version != expectedVersion) { throw SecurityException("Invalid version '${version.toInt()}' in metadata, expected '${expectedVersion.toInt()}'.") } val token = meta.getLong(JSON_METADATA_TOKEN) - if (token != expectedToken) { + if (expectedToken != null && token != expectedToken) { throw SecurityException("Invalid token '$token' in metadata, expected '$expectedToken'.") } // get package metadata diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt index 55ceead3..0080efff 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt @@ -1,6 +1,5 @@ package com.stevesoltys.seedvault.metadata -import androidx.annotation.VisibleForTesting import com.stevesoltys.seedvault.Utf8 import com.stevesoltys.seedvault.crypto.Crypto import org.json.JSONArray @@ -9,23 +8,21 @@ import java.io.IOException import java.io.OutputStream interface MetadataWriter { - @Throws(IOException::class) - fun write(outputStream: OutputStream, token: Long) + fun write(metadata: BackupMetadata, outputStream: OutputStream) + fun encode(metadata: BackupMetadata): ByteArray } -internal class MetadataWriterImpl(private val crypto: Crypto): MetadataWriter { +internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter { @Throws(IOException::class) - override fun write(outputStream: OutputStream, token: Long) { - val metadata = BackupMetadata(token = token) + override fun write(metadata: BackupMetadata, outputStream: OutputStream) { outputStream.write(ByteArray(1).apply { this[0] = metadata.version }) crypto.encryptMultipleSegments(outputStream, encode(metadata)) } - @VisibleForTesting - internal fun encode(metadata: BackupMetadata): ByteArray { + override fun encode(metadata: BackupMetadata): ByteArray { val json = JSONObject().apply { put(JSON_METADATA, JSONObject().apply { put(JSON_METADATA_VERSION, metadata.version.toInt()) diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderModule.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderModule.kt index 7392bed9..1b0f84b7 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderModule.kt @@ -6,7 +6,7 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val documentsProviderModule = module { - single { DocumentsStorage(androidContext(), get()) } + single { DocumentsStorage(androidContext(), get(), get()) } single { DocumentsProviderBackupPlugin(get(), androidContext().packageManager) } single { DocumentsProviderRestorePlugin(androidContext(), get()) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt index 567be9d8..253a1ade 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt @@ -9,6 +9,7 @@ import android.provider.DocumentsContract.* import android.provider.DocumentsContract.Document.* import android.util.Log import androidx.documentfile.provider.DocumentFile +import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.Storage import libcore.io.IoUtils.closeQuietly @@ -28,10 +29,10 @@ private val TAG = DocumentsStorage::class.java.simpleName internal class DocumentsStorage( private val context: Context, - private val settingsManager: SettingsManager) { + private val metadataManager: MetadataManager, + settingsManager: SettingsManager) { private val storage: Storage? = settingsManager.getStorage() - private val token: Long = settingsManager.getBackupToken() internal val rootBackupDir: DocumentFile? by lazy { val parent = storage?.getDocumentFile(context) ?: return@lazy null @@ -47,10 +48,7 @@ internal class DocumentsStorage( } private val currentToken: Long by lazy { - if (token != 0L) token - else settingsManager.getAndSaveNewBackupToken().apply { - Log.d(TAG, "Using a fresh backup token: $this") - } + metadataManager.getBackupToken() } private val currentSetDir: DocumentFile? by lazy { diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt index bbacac54..1506682a 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt @@ -18,6 +18,7 @@ import android.util.Log import android.view.Menu import android.view.MenuInflater import android.view.MenuItem +import androidx.lifecycle.Observer import androidx.preference.Preference import androidx.preference.Preference.OnPreferenceChangeListener import androidx.preference.PreferenceFragmentCompat @@ -28,7 +29,6 @@ import com.stevesoltys.seedvault.isMassStorage import com.stevesoltys.seedvault.restore.RestoreActivity import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.sharedViewModel -import java.util.* private val TAG = SettingsFragment::class.java.name @@ -94,6 +94,8 @@ class SettingsFragment : PreferenceFragmentCompat() { return@OnPreferenceChangeListener false } } + + viewModel.lastBackupTime.observe(this, Observer { time -> setBackupLocationSummary(time) }) } override fun onStart() { @@ -105,8 +107,8 @@ class SettingsFragment : PreferenceFragmentCompat() { storage = settingsManager.getStorage() setBackupState() setAutoRestoreState() - setBackupLocationSummary() setMenuItemStates() + viewModel.updateLastBackupTime() if (storage?.isUsb == true) context?.registerReceiver(usbReceiver, usbFilter) } @@ -159,16 +161,15 @@ class SettingsFragment : PreferenceFragmentCompat() { } } - private fun setBackupLocationSummary() { + private fun setBackupLocationSummary(lastBackupInMillis: Long) { // get name of storage location val storageName = storage?.name ?: getString(R.string.settings_backup_location_none) - // get time of last backup - val lastBackupInMillis = settingsManager.getBackupTime() + // set time of last backup val lastBackup = if (lastBackupInMillis == 0L) { getString(R.string.settings_backup_last_backup_never) } else { - getRelativeTimeSpanString(lastBackupInMillis, Date().time, MINUTE_IN_MILLIS, 0) + getRelativeTimeSpanString(lastBackupInMillis, System.currentTimeMillis(), MINUTE_IN_MILLIS, 0) } backupLocation.summary = getString(R.string.settings_backup_location_summary, storageName, lastBackup) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt index e655692b..0c236151 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -5,7 +5,6 @@ import android.hardware.usb.UsbDevice import android.net.Uri import androidx.documentfile.provider.DocumentFile import androidx.preference.PreferenceManager -import java.util.* private const val PREF_KEY_STORAGE_URI = "storageUri" private const val PREF_KEY_STORAGE_NAME = "storageName" @@ -16,9 +15,6 @@ private const val PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER = "flashSerialNumber" private const val PREF_KEY_FLASH_DRIVE_VENDOR_ID = "flashDriveVendorId" private const val PREF_KEY_FLASH_DRIVE_PRODUCT_ID = "flashDriveProductId" -private const val PREF_KEY_BACKUP_TOKEN = "backupToken" -private const val PREF_KEY_BACKUP_TIME = "backupTime" - class SettingsManager(context: Context) { private val prefs = PreferenceManager.getDefaultSharedPreferences(context) @@ -66,48 +62,6 @@ class SettingsManager(context: Context) { return FlashDrive(name, serialNumber, vendorId, productId) } - /** - * Generates and returns a new backup token while saving it as well. - * Subsequent calls to [getBackupToken] will return this new token once saved. - */ - fun getAndSaveNewBackupToken(): Long = Date().time.apply { - prefs.edit() - .putLong(PREF_KEY_BACKUP_TOKEN, this) - .apply() - } - - /** - * Returns the current backup token or 0 if none exists. - */ - fun getBackupToken(): Long { - return prefs.getLong(PREF_KEY_BACKUP_TOKEN, 0L) - } - - /** - * Sets the last backup time to "now". - */ - fun saveNewBackupTime() { - prefs.edit() - .putLong(PREF_KEY_BACKUP_TIME, Date().time) - .apply() - } - - /** - * Sets the last backup time to "never". - */ - fun resetBackupTime() { - prefs.edit() - .putLong(PREF_KEY_BACKUP_TIME, 0L) - .apply() - } - - /** - * Returns the last backup time in unix epoch milli seconds. - */ - fun getBackupTime(): Long { - return prefs.getLong(PREF_KEY_BACKUP_TIME, 0L) - } - } data class Storage( diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index 50f39677..d83f1fd2 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -1,19 +1,30 @@ package com.stevesoltys.seedvault.settings import android.app.Application +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import com.stevesoltys.seedvault.crypto.KeyManager +import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.transport.requestBackup import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel class SettingsViewModel( app: Application, settingsManager: SettingsManager, - keyManager: KeyManager + keyManager: KeyManager, + private val metadataManager: MetadataManager ) : RequireProvisioningViewModel(app, settingsManager, keyManager) { override val isRestoreOperation = false - fun backupNow() { + private val _lastBackupTime = MutableLiveData() + internal val lastBackupTime: LiveData = _lastBackupTime + + internal fun updateLastBackupTime() { + Thread { _lastBackupTime.postValue(metadataManager.getLastBackupTime()) }.start() + } + + internal fun backupNow() { Thread { requestBackup(app) }.start() } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt index 7a9e26f5..29553fee 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt @@ -7,7 +7,7 @@ import android.os.ParcelFileDescriptor import android.util.Log import com.stevesoltys.seedvault.BackupNotificationManager import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER -import com.stevesoltys.seedvault.metadata.MetadataWriter +import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.settings.SettingsManager import java.io.IOException import java.util.concurrent.TimeUnit.DAYS @@ -23,7 +23,7 @@ internal class BackupCoordinator( private val plugin: BackupPlugin, private val kv: KVBackup, private val full: FullBackup, - private val metadataWriter: MetadataWriter, + private val metadataManager: MetadataManager, private val settingsManager: SettingsManager, private val nm: BackupNotificationManager) { @@ -56,7 +56,7 @@ internal class BackupCoordinator( Log.i(TAG, "Initialize Device!") return try { plugin.initializeDevice() - writeBackupMetadata(settingsManager.getBackupToken()) + metadataManager.onDeviceInitialization(plugin.getMetadataOutputStream()) // [finishBackup] will only be called when we return [TRANSPORT_OK] here // so we remember that we initialized successfully calledInitialize = true @@ -102,15 +102,15 @@ internal class BackupCoordinator( } fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int { + val packageName = packageInfo.packageName // backups of package manager metadata do not respect backoff // we need to reject them manually when now is not a good time for a backup - if (packageInfo.packageName == MAGIC_PACKAGE_MANAGER && getBackupBackoff() != 0L) { + if (packageName == MAGIC_PACKAGE_MANAGER && getBackupBackoff() != 0L) { return TRANSPORT_PACKAGE_REJECTED } val result = kv.performBackup(packageInfo, data, flags) - if (result == TRANSPORT_OK) settingsManager.saveNewBackupTime() - return result + return onPackageBackedUp(result, packageInfo) } // ------------------------------------------------------------------------------------ @@ -138,8 +138,7 @@ internal class BackupCoordinator( fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int): Int { val result = full.performFullBackup(targetPackage, fileDescriptor, flags) - if (result == TRANSPORT_OK) settingsManager.saveNewBackupTime() - return result + return onPackageBackedUp(result, targetPackage) } fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes) @@ -193,10 +192,17 @@ internal class BackupCoordinator( else -> throw IllegalStateException("Unexpected state in finishBackup()") } - @Throws(IOException::class) - private fun writeBackupMetadata(token: Long) { - val outputStream = plugin.getMetadataOutputStream() - metadataWriter.write(outputStream, token) + private fun onPackageBackedUp(result: Int, packageInfo: PackageInfo): Int { + if (result != TRANSPORT_OK) return result + val packageName = packageInfo.packageName + try { + val outputStream = plugin.getMetadataOutputStream() + metadataManager.onPackageBackedUp(packageName, outputStream) + } catch (e: IOException) { + Log.e(TAG, "Error while writing metadata for $packageName", e) + return TRANSPORT_PACKAGE_REJECTED + } + return result } private fun getBackupBackoff(): Long { diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt index 0e599f28..d3531129 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt @@ -10,8 +10,8 @@ import android.os.ParcelFileDescriptor import android.util.Log import com.stevesoltys.seedvault.header.UnsupportedVersionException import com.stevesoltys.seedvault.metadata.DecryptionFailedException +import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.MetadataReader -import com.stevesoltys.seedvault.settings.SettingsManager import libcore.io.IoUtils.closeQuietly import java.io.IOException @@ -22,7 +22,7 @@ private class RestoreCoordinatorState( private val TAG = RestoreCoordinator::class.java.simpleName internal class RestoreCoordinator( - private val settingsManager: SettingsManager, + private val metadataManager: MetadataManager, private val plugin: RestorePlugin, private val kv: KVRestore, private val full: FullRestore, @@ -76,7 +76,7 @@ internal class RestoreCoordinator( * or 0 if there is no backup set available corresponding to the current device state. */ fun getCurrentRestoreSet(): Long { - return settingsManager.getBackupToken() + return metadataManager.getBackupToken() .apply { Log.i(TAG, "Got current restore set token: $this") } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt index 1271d42c..35e9d468 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt @@ -25,10 +25,7 @@ internal class BackupStorageViewModel( override fun onLocationSet(uri: Uri) { val isUsb = saveStorage(uri) - // use a new backup token - settingsManager.getAndSaveNewBackupToken() - - // initialize the new location + // initialize the new location, will also generate a new backup token val observer = InitializationObserver() backupManager.initializeTransportsForUser(UserHandle.myUserId(), arrayOf(TRANSPORT_ID), observer) diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageViewModel.kt index 01aeec79..85706425 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageViewModel.kt @@ -96,9 +96,6 @@ internal abstract class StorageViewModel( val storage = Storage(uri, name, root.isUsb) settingsManager.setStorage(storage) - // reset time of last backup to "Never" - settingsManager.resetBackupTime() - if (storage.isUsb) { Log.d(TAG, "Selected storage is a removable USB device.") val wasSaved = saveUsbDevice() diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt new file mode 100644 index 00000000..5f2dc5b4 --- /dev/null +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt @@ -0,0 +1,124 @@ +package com.stevesoltys.seedvault.metadata + +import android.content.Context +import android.content.Context.MODE_PRIVATE +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.stevesoltys.seedvault.Clock +import com.stevesoltys.seedvault.getRandomByteArray +import com.stevesoltys.seedvault.getRandomString +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.core.context.stopKoin +import java.io.* + +@RunWith(AndroidJUnit4::class) +class MetadataManagerTest { + + private val context: Context = mockk() + private val clock: Clock = mockk() + private val metadataWriter: MetadataWriter = mockk() + private val metadataReader: MetadataReader = mockk() + + private val manager = MetadataManager(context, clock, metadataWriter, metadataReader) + + private val time = 42L + private val initialMetadata = BackupMetadata(token = time) + private val storageOutputStream = ByteArrayOutputStream() + private val cacheOutputStream: FileOutputStream = mockk() + private val cacheInputStream: FileInputStream = mockk() + private val encodedMetadata = getRandomByteArray() + + @After + fun afterEachTest() { + stopKoin() + } + + @Test + fun `test onDeviceInitialization()`() { + every { clock.time() } returns time + every { metadataWriter.write(initialMetadata, storageOutputStream) } just Runs + expectWriteToCache(initialMetadata) + + manager.onDeviceInitialization(storageOutputStream) + + assertEquals(time, manager.getBackupToken()) + assertEquals(0L, manager.getLastBackupTime()) + } + + @Test + fun `test onPackageBackedUp()`() { + val packageName = getRandomString() + val updatedMetadata = initialMetadata.copy() + updatedMetadata.time = time + updatedMetadata.packageMetadata[packageName] = PackageMetadata(time) + + every { context.openFileInput(METADATA_CACHE_FILE) } throws FileNotFoundException() + every { clock.time() } returns time + every { metadataWriter.write(updatedMetadata, storageOutputStream) } just Runs + expectWriteToCache(updatedMetadata) + + manager.onPackageBackedUp(packageName, storageOutputStream) + + assertEquals(time, manager.getLastBackupTime()) + } + + @Test + fun `test onPackageBackedUp() fails to write to storage`() { + val packageName = getRandomString() + val updatedMetadata = initialMetadata.copy() + updatedMetadata.time = time + updatedMetadata.packageMetadata[packageName] = PackageMetadata(time) + + every { context.openFileInput(METADATA_CACHE_FILE) } throws FileNotFoundException() + every { clock.time() } returns time + every { metadataWriter.write(updatedMetadata, storageOutputStream) } throws IOException() + + try { + manager.onPackageBackedUp(packageName, storageOutputStream) + fail() + } catch (e: IOException) { + // expected + } + + assertEquals(0L, manager.getLastBackupTime()) // time was reverted + // TODO also assert reverted PackageMetadata once possible + } + + @Test + fun `test onPackageBackedUp() with filled cache`() { + val cachedPackageName = getRandomString() + val packageName = getRandomString() + val byteArray = ByteArray(DEFAULT_BUFFER_SIZE) + + val cachedMetadata = initialMetadata.copy(time = 23) + cachedMetadata.packageMetadata[cachedPackageName] = PackageMetadata(23) + cachedMetadata.packageMetadata[packageName] = PackageMetadata(23) + + every { context.openFileInput(METADATA_CACHE_FILE) } returns cacheInputStream + every { cacheInputStream.available() } returns byteArray.size andThen 0 + every { cacheInputStream.read(byteArray) } returns -1 + every { metadataReader.decode(ByteArray(0)) } returns cachedMetadata + every { clock.time() } returns time + every { metadataWriter.write(cachedMetadata, storageOutputStream) } just Runs + expectWriteToCache(cachedMetadata) + + manager.onPackageBackedUp(packageName, storageOutputStream) + + assertEquals(time, manager.getLastBackupTime()) + // TODO also assert updated PackageMetadata once possible + } + + private fun expectWriteToCache(metadata: BackupMetadata) { + every { metadataWriter.encode(metadata) } returns encodedMetadata + every { context.openFileOutput(METADATA_CACHE_FILE, MODE_PRIVATE) } returns cacheOutputStream + every { cacheOutputStream.write(encodedMetadata) } just Runs + } + +} diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReaderTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReaderTest.kt index c38e6d78..8552c26e 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReaderTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReaderTest.kt @@ -122,7 +122,7 @@ class MetadataReaderTest { assertNull(packageMetadata.signatures) } - private fun getMetadata(packageMetadata: Map = HashMap()): BackupMetadata { + private fun getMetadata(packageMetadata: HashMap = HashMap()): BackupMetadata { return BackupMetadata( version = 1.toByte(), token = Random.nextLong(), diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt index b525d181..6d5b7e87 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt @@ -66,7 +66,7 @@ internal class MetadataWriterDecoderTest { assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token)) } - private fun getMetadata(packageMetadata: Map = HashMap()): BackupMetadata { + private fun getMetadata(packageMetadata: HashMap = HashMap()): BackupMetadata { return BackupMetadata( version = Random.nextBytes(1)[0], token = Random.nextLong(), diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt index 95ccfa87..6be29db8 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt @@ -16,7 +16,6 @@ import com.stevesoltys.seedvault.header.HeaderReaderImpl import com.stevesoltys.seedvault.header.HeaderWriterImpl import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH import com.stevesoltys.seedvault.metadata.MetadataReaderImpl -import com.stevesoltys.seedvault.metadata.MetadataWriterImpl import com.stevesoltys.seedvault.transport.backup.* import com.stevesoltys.seedvault.transport.restore.* import io.mockk.* @@ -35,7 +34,6 @@ internal class CoordinatorIntegrationTest : TransportTest() { private val headerWriter = HeaderWriterImpl() private val headerReader = HeaderReaderImpl() private val cryptoImpl = CryptoImpl(cipherFactory, headerWriter, headerReader) - private val metadataWriter = MetadataWriterImpl(cryptoImpl) private val metadataReader = MetadataReaderImpl(cryptoImpl) private val backupPlugin = mockk() @@ -44,20 +42,21 @@ internal class CoordinatorIntegrationTest : TransportTest() { private val fullBackupPlugin = mockk() private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl) private val notificationManager = mockk() - private val backup = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, metadataWriter, settingsManager, notificationManager) + private val backup = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, metadataManager, settingsManager, notificationManager) private val restorePlugin = mockk() private val kvRestorePlugin = mockk() private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl) private val fullRestorePlugin = mockk() private val fullRestore = FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl) - private val restore = RestoreCoordinator(settingsManager, restorePlugin, kvRestore, fullRestore, metadataReader) + private val restore = RestoreCoordinator(metadataManager, restorePlugin, kvRestore, fullRestore, metadataReader) private val backupDataInput = mockk() private val fileDescriptor = mockk(relaxed = true) private val token = Random.nextLong() private val appData = ByteArray(42).apply { Random.nextBytes(this) } private val appData2 = ByteArray(1337).apply { Random.nextBytes(this) } + private val metadataOutputStream = ByteArrayOutputStream() private val key = "RestoreKey" private val key64 = key.encodeBase64() private val key2 = "RestoreKey2" @@ -92,7 +91,8 @@ internal class CoordinatorIntegrationTest : TransportTest() { appData2.size } every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key264) } returns bOutputStream2 - every { settingsManager.saveNewBackupTime() } just Runs + every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream + every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs // start and finish K/V backup assertEquals(TRANSPORT_OK, backup.performIncrementalBackup(packageInfo, fileDescriptor, 0)) @@ -179,7 +179,8 @@ internal class CoordinatorIntegrationTest : TransportTest() { every { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP - every { settingsManager.saveNewBackupTime() } just Runs + every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream + every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs // perform backup to output stream assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, fileDescriptor, 0)) diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt index 6858f600..9e307916 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.pm.PackageInfo import android.util.Log import com.stevesoltys.seedvault.crypto.Crypto +import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.settings.SettingsManager import io.mockk.every import io.mockk.mockk @@ -16,6 +17,7 @@ abstract class TransportTest { protected val crypto = mockk() protected val settingsManager = mockk() + protected val metadataManager = mockk() protected val context = mockk(relaxed = true) protected val packageInfo = PackageInfo().apply { packageName = "org.example" } diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt index ee843d61..96d666b4 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt @@ -6,7 +6,6 @@ import android.net.Uri import androidx.documentfile.provider.DocumentFile import com.stevesoltys.seedvault.BackupNotificationManager import com.stevesoltys.seedvault.getRandomString -import com.stevesoltys.seedvault.metadata.MetadataWriter import com.stevesoltys.seedvault.settings.Storage import io.mockk.Runs import io.mockk.every @@ -24,18 +23,17 @@ internal class BackupCoordinatorTest: BackupTest() { private val plugin = mockk() private val kv = mockk() private val full = mockk() - private val metadataWriter = mockk() private val notificationManager = mockk() - private val backup = BackupCoordinator(context, plugin, kv, full, metadataWriter, settingsManager, notificationManager) + private val backup = BackupCoordinator(context, plugin, kv, full, metadataManager, settingsManager, notificationManager) private val metadataOutputStream = mockk() @Test fun `device initialization succeeds and delegates to plugin`() { every { plugin.initializeDevice() } just Runs - every { settingsManager.getBackupToken() } returns token - expectWritingMetadata(token) + every { plugin.getMetadataOutputStream() } returns metadataOutputStream + every { metadataManager.onDeviceInitialization(metadataOutputStream) } just Runs every { kv.hasState() } returns false every { full.hasState() } returns false @@ -145,9 +143,4 @@ internal class BackupCoordinatorTest: BackupTest() { assertEquals(result, backup.finishBackup()) } - private fun expectWritingMetadata(token: Long = this.token) { - every { plugin.getMetadataOutputStream() } returns metadataOutputStream - every { metadataWriter.write(metadataOutputStream, token) } just Runs - } - } diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt index 313d4fb9..50117f4f 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt @@ -27,7 +27,7 @@ internal class RestoreCoordinatorTest : TransportTest() { private val full = mockk() private val metadataReader = mockk() - private val restore = RestoreCoordinator(settingsManager, plugin, kv, full, metadataReader) + private val restore = RestoreCoordinator(metadataManager, plugin, kv, full, metadataReader) private val token = Random.nextLong() private val inputStream = mockk() @@ -57,7 +57,7 @@ internal class RestoreCoordinatorTest : TransportTest() { @Test fun `getCurrentRestoreSet() delegates to plugin`() { - every { settingsManager.getBackupToken() } returns token + every { metadataManager.getBackupToken() } returns token assertEquals(token, restore.getCurrentRestoreSet()) }