From 237fd683bd5ad404393aa9f05a9dcc9644f6c748 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 13 Sep 2024 12:07:34 -0300 Subject: [PATCH] Clean up metadata as it lost most of its importance Historically, metadata was uploaded to the backend after each app update and contained all essential data that is now in snapshots. We still support reading metadata for legacy backups and use the metadata classes as a common wrapper for snapshots. However, there is no need anymore to write out complete historic metadata and maintain duplicated unused information there. This got removed. THe information we do still save and write out is only for UI representation of backup state. The time of last backup is now managed by SettingsManager. --- .../seedvault/e2e/LargeTestBase.kt | 1 - .../seedvault/UsbIntentReceiver.kt | 9 +- .../seedvault/metadata/MetadataManager.kt | 84 +----- .../seedvault/metadata/MetadataModule.kt | 4 +- .../seedvault/metadata/MetadataReader.kt | 14 +- .../seedvault/metadata/MetadataWriter.kt | 45 +--- .../seedvault/settings/SettingsManager.kt | 32 ++- .../seedvault/settings/SettingsViewModel.kt | 19 +- .../transport/backup/AppBackupManager.kt | 15 +- .../transport/backup/BackupCoordinator.kt | 14 - .../transport/backup/BackupModule.kt | 2 +- .../transport/backup/SnapshotCreator.kt | 7 +- .../com/stevesoltys/seedvault/ui/UiUtils.kt | 2 +- .../NotificationBackupObserver.kt | 14 +- .../seedvault/metadata/MetadataManagerTest.kt | 249 +----------------- .../metadata/MetadataReadWriteTest.kt | 76 ------ .../seedvault/metadata/MetadataReaderTest.kt | 98 +------ .../metadata/MetadataWriterDecoderTest.kt | 161 ----------- .../transport/CoordinatorIntegrationTest.kt | 2 - .../transport/backup/BackupCoordinatorTest.kt | 26 -- .../seedvault/worker/ApkBackupManagerTest.kt | 1 - 21 files changed, 82 insertions(+), 793 deletions(-) delete mode 100644 app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReadWriteTest.kt delete mode 100644 app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt index 809834e9..1f7d63cc 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/e2e/LargeTestBase.kt @@ -85,7 +85,6 @@ internal interface LargeTestBase : KoinComponent { fun resetApplicationState() { backupManager.setAutoRestore(false) - settingsManager.token = null val sharedPreferences = permitDiskReads { PreferenceManager.getDefaultSharedPreferences(targetContext) diff --git a/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt b/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt index 0cfcfcde..418f795f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt @@ -20,7 +20,6 @@ import android.os.Handler import android.os.Looper 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.ui.storage.AUTHORITY_STORAGE @@ -34,7 +33,6 @@ class UsbIntentReceiver : UsbMonitor() { // using KoinComponent would crash robolectric tests :( private val settingsManager: SettingsManager by lazy { get().get() } - private val metadataManager: MetadataManager by lazy { get().get() } private val backupManager: IBackupManager by lazy { get().get() } override fun shouldMonitorStatus(context: Context, action: String, device: UsbDevice): Boolean { @@ -44,14 +42,15 @@ class UsbIntentReceiver : UsbMonitor() { val attachedFlashDrive = FlashDrive.from(device) return if (savedFlashDrive == attachedFlashDrive) { Log.d(TAG, "Matches stored device, checking backup time...") - val backupMillis = System.currentTimeMillis() - metadataManager.getLastBackupTime() + val lastBackupTime = settingsManager.lastBackupTime.value ?: 0 + val backupMillis = System.currentTimeMillis() - lastBackupTime if (backupMillis >= settingsManager.backupFrequencyInMillis) { Log.d(TAG, "Last backup older than it should be, requesting a backup...") - Log.d(TAG, " ${Date(metadataManager.getLastBackupTime())}") + Log.d(TAG, " ${Date(lastBackupTime)}") true } else { Log.d(TAG, "We have a recent backup, not requesting a new one.") - Log.d(TAG, " ${Date(metadataManager.getLastBackupTime())}") + Log.d(TAG, " ${Date(lastBackupTime)}") false } } else { diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt index c3e66416..cc9655a6 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt @@ -11,13 +11,8 @@ import android.content.pm.PackageInfo import android.util.Log import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.distinctUntilChanged import com.stevesoltys.seedvault.Clock -import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA -import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.transport.backup.isSystemApp import java.io.FileNotFoundException @@ -37,7 +32,6 @@ internal class MetadataManager( private val metadataWriter: MetadataWriter, private val metadataReader: MetadataReader, private val packageService: PackageService, - private val settingsManager: SettingsManager, ) { private val uninitializedMetadata = BackupMetadata(token = -42L, salt = "foo bar") @@ -54,7 +48,6 @@ internal class MetadataManager( // This should cause requiresInit() return true uninitializedMetadata.copy(version = (-1).toByte()) } - mLastBackupTime.postValue(field.time) } return field } @@ -63,40 +56,6 @@ internal class MetadataManager( packageService.launchableSystemApps.map { it.activityInfo.packageName }.toSet() } - /** - * Call this after a package's APK has been backed up successfully. - * - * It updates the packages' metadata to the internal cache. - */ - @Synchronized - @Throws(IOException::class) - fun onApkBackedUp( - packageInfo: PackageInfo, - packageMetadata: PackageMetadata, - ) { - val packageName = packageInfo.packageName - metadata.packageMetadataMap[packageName]?.let { - check(packageMetadata.version != null) { - "APK backup returned version null" - } - } - val oldPackageMetadata = metadata.packageMetadataMap[packageName] - ?: PackageMetadata() - modifyCachedMetadata { - val isSystemApp = packageInfo.isSystemApp() - metadata.packageMetadataMap[packageName] = oldPackageMetadata.copy( - name = packageInfo.applicationInfo?.loadLabel(context.packageManager), - system = isSystemApp, - isLaunchableSystemApp = isSystemApp && launchableSystemApps.contains(packageName), - version = packageMetadata.version, - installer = packageMetadata.installer, - splits = packageMetadata.splits, - sha256 = packageMetadata.sha256, - signatures = packageMetadata.signatures - ) - } - } - /** * Call this after a package has been backed up successfully. * @@ -115,8 +74,6 @@ internal class MetadataManager( val packageName = packageInfo.packageName modifyCachedMetadata { val now = clock.time() - metadata.time = now - metadata.d2dBackup = settingsManager.d2dBackupsEnabled() metadata.packageMetadataMap.getOrPut(packageName) { val isSystemApp = packageInfo.isSystemApp() PackageMetadata( @@ -124,7 +81,6 @@ internal class MetadataManager( state = APK_AND_DATA, backupType = type, size = size, - name = packageInfo.applicationInfo?.loadLabel(context.packageManager), system = isSystemApp, isLaunchableSystemApp = isSystemApp && launchableSystemApps.contains(packageName), @@ -135,10 +91,6 @@ internal class MetadataManager( backupType = type // don't override a previous K/V size, if there were no K/V changes if (size != null) this.size = size - // update name, if none was set, yet (can happen while migrating to storing names) - if (this.name == null) { - this.name = packageInfo.applicationInfo?.loadLabel(context.packageManager) - } } } } @@ -203,9 +155,15 @@ internal class MetadataManager( } } + @Synchronized + fun getPackageMetadata(packageName: String): PackageMetadata? { + return metadata.packageMetadataMap[packageName]?.copy() + } + @Throws(IOException::class) private fun modifyCachedMetadata(modFun: () -> Unit) { - val oldMetadata = metadata.copy( // copy map, otherwise it will re-use same reference + val oldMetadata = metadata.copy( + // copy map, otherwise it will re-use same reference packageMetadataMap = PackageMetadataMap(metadata.packageMetadataMap), ) try { @@ -217,34 +175,6 @@ internal class MetadataManager( metadata = oldMetadata throw IOException(e) } - mLastBackupTime.postValue(metadata.time) // TODO only do after snapshot was written - } - - /** - * Returns the last backup time in unix epoch milli seconds. - * - * Note that this might be a blocking I/O call. - */ - @Synchronized - fun getLastBackupTime(): Long = mLastBackupTime.value ?: metadata.time - - private val mLastBackupTime = MutableLiveData() - internal val lastBackupTime: LiveData = mLastBackupTime.distinctUntilChanged() - - internal val salt: String - @Synchronized get() = metadata.salt - - internal val requiresInit: Boolean - @Synchronized get() = metadata == uninitializedMetadata || metadata.version < VERSION - - @Synchronized - fun getPackageMetadata(packageName: String): PackageMetadata? { - return metadata.packageMetadataMap[packageName]?.copy() - } - - @Synchronized - fun getPackagesBackupSize(): Long { - return metadata.packageMetadataMap.values.sumOf { it.size ?: 0L } } @Synchronized 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 b0a10173..003fff7e 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt @@ -9,7 +9,7 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val metadataModule = module { - single { MetadataManager(androidContext(), get(), get(), get(), get(), get()) } - single { MetadataWriterImpl(get()) } + single { MetadataManager(androidContext(), get(), get(), get(), get()) } + single { MetadataWriterImpl() } 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 61cbac94..72389a29 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt @@ -94,14 +94,14 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader { val json = JSONObject(bytes.toString(Utf8)) // get backup metadata and check expectations val meta = json.getJSONObject(JSON_METADATA) - val version = meta.getInt(JSON_METADATA_VERSION).toByte() + val version = meta.optInt(JSON_METADATA_VERSION, VERSION.toInt()).toByte() if (expectedVersion != null && version != expectedVersion) { throw SecurityException( "Invalid version '${version.toInt()}' in metadata," + "expected '${expectedVersion.toInt()}'." ) } - val token = meta.getLong(JSON_METADATA_TOKEN) + val token = meta.optLong(JSON_METADATA_TOKEN, 0) if (expectedToken != null && token != expectedToken) throw SecurityException( "Invalid token '$token' in metadata, expected '$expectedToken'." ) @@ -157,11 +157,11 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader { return BackupMetadata( version = version, token = token, - salt = if (version == 0.toByte()) "" else meta.getString(JSON_METADATA_SALT), - time = meta.getLong(JSON_METADATA_TIME), - androidVersion = meta.getInt(JSON_METADATA_SDK_INT), - androidIncremental = meta.getString(JSON_METADATA_INCREMENTAL), - deviceName = meta.getString(JSON_METADATA_NAME), + salt = if (version == 0.toByte()) "" else meta.optString(JSON_METADATA_SALT, ""), + time = meta.optLong(JSON_METADATA_TIME, -1), + androidVersion = meta.optInt(JSON_METADATA_SDK_INT, 0), + androidIncremental = meta.optString(JSON_METADATA_INCREMENTAL), + deviceName = meta.optString(JSON_METADATA_NAME), d2dBackup = meta.optBoolean(JSON_METADATA_D2D_BACKUP, false), packageMetadataMap = packageMetadataMap, ) 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 2cbb6ace..ce9c317b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt @@ -6,42 +6,18 @@ package com.stevesoltys.seedvault.metadata import com.stevesoltys.seedvault.Utf8 -import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA -import org.json.JSONArray import org.json.JSONObject -import java.io.IOException -import java.io.OutputStream interface MetadataWriter { - @Throws(IOException::class) - fun write(metadata: BackupMetadata, outputStream: OutputStream) - fun encode(metadata: BackupMetadata): ByteArray } -internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter { - - @Throws(IOException::class) - override fun write(metadata: BackupMetadata, outputStream: OutputStream) { - outputStream.write(ByteArray(1).apply { this[0] = metadata.version }) - crypto.newEncryptingStreamV1(outputStream, getAD(metadata.version, metadata.token)).use { - it.write(encode(metadata)) - } - } +internal class MetadataWriterImpl : MetadataWriter { override fun encode(metadata: BackupMetadata): ByteArray { val json = JSONObject().apply { - put(JSON_METADATA, JSONObject().apply { - put(JSON_METADATA_VERSION, metadata.version.toInt()) - put(JSON_METADATA_TOKEN, metadata.token) - put(JSON_METADATA_SALT, metadata.salt) - put(JSON_METADATA_TIME, metadata.time) - put(JSON_METADATA_SDK_INT, metadata.androidVersion) - put(JSON_METADATA_INCREMENTAL, metadata.androidIncremental) - put(JSON_METADATA_NAME, metadata.deviceName) - put(JSON_METADATA_D2D_BACKUP, metadata.d2dBackup) - }) + put(JSON_METADATA, JSONObject()) } for ((packageName, packageMetadata) in metadata.packageMetadataMap) { json.put(packageName, JSONObject().apply { @@ -57,31 +33,14 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter { if (packageMetadata.size != null) { put(JSON_PACKAGE_SIZE, packageMetadata.size) } - if (packageMetadata.name != null) { - put(JSON_PACKAGE_APP_NAME, packageMetadata.name) - } if (packageMetadata.system) { put(JSON_PACKAGE_SYSTEM, true) } if (packageMetadata.isLaunchableSystemApp) { put(JSON_PACKAGE_SYSTEM_LAUNCHER, true) } - packageMetadata.version?.let { put(JSON_PACKAGE_VERSION, it) } - packageMetadata.installer?.let { put(JSON_PACKAGE_INSTALLER, it) } - packageMetadata.splits?.let { splits -> - put(JSON_PACKAGE_SPLITS, JSONArray().apply { - for (split in splits) put(JSONObject().apply { - put(JSON_PACKAGE_SPLIT_NAME, split.name) - if (split.size != null) put(JSON_PACKAGE_SIZE, split.size) - put(JSON_PACKAGE_SHA256, split.sha256) - }) - }) - } - packageMetadata.sha256?.let { put(JSON_PACKAGE_SHA256, it) } - packageMetadata.signatures?.let { put(JSON_PACKAGE_SIGNATURES, JSONArray(it)) } }) } return json.toString().toByteArray(Utf8) } - } 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 edcba386..25a23706 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -10,6 +10,8 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.hardware.usb.UsbDevice import android.net.Uri import androidx.annotation.UiThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.preference.PreferenceManager import com.stevesoltys.seedvault.backend.webdav.WebDavHandler.Companion.createWebDavProperties import com.stevesoltys.seedvault.permitDiskReads @@ -55,20 +57,19 @@ private const val PREF_KEY_BACKUP_APP_BLACKLIST = "backupAppBlacklist" private const val PREF_KEY_BACKUP_STORAGE = "backup_storage" internal const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota" internal const val PREF_KEY_D2D_BACKUPS = "d2d_backups" +internal const val PREF_KEY_LAST_BACKUP = "lastBackup" class SettingsManager(private val context: Context) { private val prefs = permitDiskReads { PreferenceManager.getDefaultSharedPreferences(context) } + private val mLastBackupTime = MutableLiveData(prefs.getLong(PREF_KEY_LAST_BACKUP, -1)) - fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) { - prefs.registerOnSharedPreferenceChangeListener(listener) - } - - fun unregisterOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) { - prefs.unregisterOnSharedPreferenceChangeListener(listener) - } + /** + * Returns a LiveData of the last backup time in unix epoch milli seconds. + */ + internal val lastBackupTime: LiveData = mLastBackupTime /** * This gets accessed by non-UI threads when saving with [PreferenceManager] @@ -81,7 +82,7 @@ class SettingsManager(private val context: Context) { @Volatile var token: Long? = null - set(newToken) { + private set(newToken) { if (newToken == null) { prefs.edit() .remove(PREF_KEY_TOKEN) @@ -121,6 +122,21 @@ class SettingsManager(private val context: Context) { } } + fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) { + prefs.registerOnSharedPreferenceChangeListener(listener) + } + + fun unregisterOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) { + prefs.unregisterOnSharedPreferenceChangeListener(listener) + } + + fun onSuccessfulBackupCompleted(token: Long) { + this.token = token + val now = System.currentTimeMillis() + prefs.edit().putLong(PREF_KEY_LAST_BACKUP, now).apply() + mLastBackupTime.postValue(now) + } + fun setStorageBackend(plugin: Backend) { val value = when (plugin) { is SafBackend -> StoragePluginType.SAF 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 e5ccd77e..bb1f10bb 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -88,7 +88,7 @@ internal class SettingsViewModel( private val mBackupPossible = MutableLiveData(false) val backupPossible: LiveData = mBackupPossible - internal val lastBackupTime = metadataManager.lastBackupTime + internal val lastBackupTime = settingsManager.lastBackupTime internal val appBackupWorkInfo = workManager.getWorkInfosForUniqueWorkLiveData(UNIQUE_WORK_NAME).map { it.getOrNull(0) @@ -143,8 +143,6 @@ internal class SettingsViewModel( initialValue = false, ) scope.launch { - // ensures the lastBackupTime LiveData gets set - metadataManager.getLastBackupTime() // update running state isBackupRunning.collect { onBackupRunningStateChanged() @@ -258,21 +256,6 @@ internal class SettingsViewModel( fun onBackupEnabled(enabled: Boolean) { if (enabled) { - if (metadataManager.requiresInit) { - val onError: () -> Unit = { - viewModelScope.launch(Dispatchers.Main) { - val res = R.string.storage_check_fragment_backup_error - Toast.makeText(app, res, LENGTH_LONG).show() - } - } - viewModelScope.launch(Dispatchers.IO) { - backupInitializer.initialize(onError) { - mInitEvent.postEvent(false) - scheduleAppBackup(CANCEL_AND_REENQUEUE) - } - mInitEvent.postEvent(true) - } - } // enable call log backups for existing installs (added end of 2020) enableCallLogBackup() } else { diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/AppBackupManager.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/AppBackupManager.kt index 1c89d2a0..d3eb96eb 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/AppBackupManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/AppBackupManager.kt @@ -5,6 +5,7 @@ package com.stevesoltys.seedvault.transport.backup +import androidx.annotation.WorkerThread import com.stevesoltys.seedvault.backend.BackendManager import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.settings.SettingsManager @@ -29,6 +30,7 @@ internal class AppBackupManager( var snapshotCreator: SnapshotCreator? = null private set + @WorkerThread suspend fun beforeBackup() { log.info { "Loading existing snapshots and blobs..." } val blobInfos = mutableListOf() @@ -48,25 +50,26 @@ internal class AppBackupManager( blobCache.populateCache(blobInfos, snapshots) } - suspend fun afterBackupFinished(success: Boolean): Boolean { + @WorkerThread + suspend fun afterBackupFinished(success: Boolean): com.stevesoltys.seedvault.proto.Snapshot? { log.info { "After backup finished. Success: $success" } // free up memory by clearing blobs cache blobCache.clear() - var result = false - try { + return try { if (success) { val snapshot = snapshotCreator?.finalizeSnapshot() ?: error("Had no snapshotCreator") keepTrying { // saving this is so important, we even keep trying snapshotManager.saveSnapshot(snapshot) } - settingsManager.token = snapshot.token + settingsManager.onSuccessfulBackupCompleted(snapshot.token) // after snapshot was written, we can clear local cache as its info is in snapshot blobCache.clearLocalCache() - } - result = true + snapshot + } else null } catch (e: Exception) { log.error(e) { "Error finishing backup" } + null } finally { snapshotCreator = null } 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 e8df2afa..a4a7312b 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 @@ -199,15 +199,6 @@ internal class BackupCoordinator( flags: Int, ): Int { state.cancelReason = UNKNOWN_ERROR - if (metadataManager.requiresInit) { - Log.w(TAG, "Metadata requires re-init!") - // Tell the system that we are not initialized, it will initialize us afterwards. - // This will start a new restore set to upgrade from legacy format - // by starting a clean backup with all files using the new version. - // - // This causes a backup error, but things should go back to normal afterwards. - return TRANSPORT_NOT_INITIALIZED - } return kv.performBackup(packageInfo, data, flags) } @@ -324,8 +315,6 @@ internal class BackupCoordinator( // tell K/V backup to finish val backupData = kv.finishBackup() snapshotCreator.onPackageBackedUp(packageInfo, BackupType.KV, backupData) - // TODO unify both calls - metadataManager.onPackageBackedUp(packageInfo, BackupType.KV, backupData.size) TRANSPORT_OK } catch (e: Exception) { Log.e(TAG, "Error finishing K/V backup for $packageName", e) @@ -345,8 +334,6 @@ internal class BackupCoordinator( try { val backupData = full.finishBackup() snapshotCreator.onPackageBackedUp(packageInfo, BackupType.FULL, backupData) - // TODO unify both calls - metadataManager.onPackageBackedUp(packageInfo, BackupType.FULL, backupData.size) TRANSPORT_OK } catch (e: Exception) { Log.e(TAG, "Error calling onPackageBackedUp for $packageName", e) @@ -362,7 +349,6 @@ internal class BackupCoordinator( else -> throw IllegalStateException("Unexpected state in finishBackup()") } - // TODO is this only nice to have info, or do we need to do more? private fun onPackageBackupError(packageInfo: PackageInfo, type: BackupType) { val packageName = packageInfo.packageName try { diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt index cbde0a22..ca49095d 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt @@ -19,7 +19,7 @@ val backupModule = module { val snapshotFolder = File(androidContext().filesDir, "snapshots") SnapshotManager(snapshotFolder, get(), get(), get()) } - single { SnapshotCreatorFactory(androidContext(), get(), get(), get()) } + single { SnapshotCreatorFactory(androidContext(), get(), get(), get(), get()) } single { InputFactory() } single { PackageService( diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/SnapshotCreator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/SnapshotCreator.kt index 4c5ad3d6..b51dae2e 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/SnapshotCreator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/SnapshotCreator.kt @@ -17,6 +17,7 @@ import com.google.protobuf.ByteString import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.metadata.BackupType +import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.proto.Snapshot import com.stevesoltys.seedvault.proto.Snapshot.Apk @@ -32,8 +33,10 @@ internal class SnapshotCreatorFactory( private val clock: Clock, private val packageService: PackageService, private val settingsManager: SettingsManager, + private val metadataManager: MetadataManager, ) { - fun createSnapshotCreator() = SnapshotCreator(context, clock, packageService, settingsManager) + fun createSnapshotCreator() = + SnapshotCreator(context, clock, packageService, settingsManager, metadataManager) } internal class SnapshotCreator( @@ -41,6 +44,7 @@ internal class SnapshotCreator( private val clock: Clock, private val packageService: PackageService, private val settingsManager: SettingsManager, + private val metadataManager: MetadataManager, ) { private val log = KotlinLogging.logger { } @@ -88,6 +92,7 @@ internal class SnapshotCreator( addAllChunkIds(chunkIds) } blobsMap.putAll(backupData.chunkMap) + metadataManager.onPackageBackedUp(packageInfo, backupType, backupData.size) } fun onIconsBackedUp(backupData: BackupData) { diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/UiUtils.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/UiUtils.kt index 540efa09..2e6578d5 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/UiUtils.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/UiUtils.kt @@ -11,7 +11,7 @@ import android.text.format.DateUtils.getRelativeTimeSpanString import com.stevesoltys.seedvault.R fun Long.toRelativeTime(context: Context): CharSequence { - return if (this == 0L) { + return if (this == 0L || this == -1L) { context.getString(R.string.settings_backup_last_backup_never) } else { val now = System.currentTimeMillis() diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt index f2648531..8296887c 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt @@ -22,6 +22,7 @@ import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.transport.backup.AppBackupManager import com.stevesoltys.seedvault.transport.backup.PackageService +import com.stevesoltys.seedvault.transport.backup.hexFromProto import com.stevesoltys.seedvault.worker.BackupRequester import kotlinx.coroutines.runBlocking import org.koin.core.component.KoinComponent @@ -138,18 +139,25 @@ internal class NotificationBackupObserver( Log.i(TAG, "Backup finished $numPackages/$requestedPackages. Status: $status") } var success = status == 0 - val size = if (success) metadataManager.getPackagesBackupSize() else 0L val total = try { packageService.allUserPackages.size } catch (e: Exception) { Log.e(TAG, "Error getting number of all user packages: ", e) requestedPackages } - runBlocking { + val snapshot = runBlocking { check(!Looper.getMainLooper().isCurrentThread) Log.d(TAG, "Finalizing backup...") - success = appBackupManager.afterBackupFinished(success) + val snapshot = appBackupManager.afterBackupFinished(success) + success = snapshot != null + snapshot } + val size = if (snapshot != null) { // TODO count size of APKs separately + val chunkIds = snapshot.appsMap.values.flatMap { it.chunkIdsList } + chunkIds.sumOf { + snapshot.blobsMap[it.hexFromProto()]?.uncompressedLength?.toLong() ?: 0L + } + } else 0L nm.onBackupFinished(success, numPackagesToReport, total, size) } } diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt index 2a1526d5..368777ca 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt @@ -7,25 +7,20 @@ package com.stevesoltys.seedvault.metadata import android.content.Context import android.content.Context.MODE_PRIVATE -import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP import android.content.pm.ApplicationInfo.FLAG_SYSTEM import android.content.pm.PackageInfo import android.content.pm.PackageManager -import android.content.pm.ResolveInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.TestApp -import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.encodeBase64 import com.stevesoltys.seedvault.getRandomByteArray import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA -import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED -import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.transport.backup.PackageService @@ -37,22 +32,16 @@ import io.mockk.verify import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import org.junit.jupiter.api.assertThrows import org.junit.runner.RunWith import org.koin.core.context.stopKoin import org.robolectric.annotation.Config import java.io.ByteArrayOutputStream import java.io.FileInputStream -import java.io.FileNotFoundException import java.io.FileOutputStream -import java.io.IOException import kotlin.random.Random -@Suppress("DEPRECATION") @RunWith(AndroidJUnit4::class) @Config( sdk = [34], // TODO: Drop once robolectric supports 35 @@ -62,7 +51,6 @@ class MetadataManagerTest { private val context: Context = mockk() private val clock: Clock = mockk() - private val crypto: Crypto = mockk() private val metadataWriter: MetadataWriter = mockk() private val metadataReader: MetadataReader = mockk() private val packageService: PackageService = mockk() @@ -74,7 +62,6 @@ class MetadataManagerTest { metadataWriter = metadataWriter, metadataReader = metadataReader, packageService = packageService, - settingsManager = settingsManager, ) private val packageManager: PackageManager = mockk() @@ -104,187 +91,6 @@ class MetadataManagerTest { stopKoin() } - @Test - fun `test onApkBackedUp() with no prior package metadata`() { - val packageMetadata = PackageMetadata( - time = 0L, - version = Random.nextLong(Long.MAX_VALUE), - installer = getRandomString(), - signatures = listOf("sig") - ) - - every { context.packageManager } returns packageManager - expectReadFromCache() - expectModifyMetadata(initialMetadata) - - manager.onApkBackedUp(packageInfo, packageMetadata) - - assertEquals(packageMetadata, manager.getPackageMetadata(packageName)) - - verify { - cacheInputStream.close() - cacheOutputStream.close() - } - } - - @Test - fun `test onApkBackedUp() sets system metadata`() { - packageInfo.applicationInfo = ApplicationInfo().apply { flags = FLAG_SYSTEM } - val packageMetadata = PackageMetadata( - time = 0L, - version = Random.nextLong(Long.MAX_VALUE), - installer = getRandomString(), - signatures = listOf("sig") - ) - - every { context.packageManager } returns packageManager - every { packageService.launchableSystemApps } returns listOf( - ResolveInfo().apply { - activityInfo = ActivityInfo().apply { - packageName = this@MetadataManagerTest.packageName - } - } - ) - expectReadFromCache() - expectModifyMetadata(initialMetadata) - - manager.onApkBackedUp(packageInfo, packageMetadata) - - assertEquals( - packageMetadata.copy(system = true, isLaunchableSystemApp = true), - manager.getPackageMetadata(packageName), - ) - - verify { - cacheInputStream.close() - cacheOutputStream.close() - } - } - - @Test - fun `test onApkBackedUp() with existing package metadata`() { - val packageMetadata = PackageMetadata( - time = time, - version = Random.nextLong(Long.MAX_VALUE), - installer = getRandomString(), - signatures = listOf("sig") - ) - initialMetadata.packageMetadataMap[packageName] = packageMetadata - val updatedPackageMetadata = PackageMetadata( - time = time, - version = packageMetadata.version!! + 1, - installer = getRandomString(), - signatures = listOf("sig foo") - ) - - every { context.packageManager } returns packageManager - expectReadFromCache() - expectWriteToCache(initialMetadata) - - manager.onApkBackedUp(packageInfo, updatedPackageMetadata) - - assertEquals(updatedPackageMetadata, manager.getPackageMetadata(packageName)) - - verify { - cacheInputStream.close() - cacheOutputStream.close() - } - } - - @Test - fun `test onApkBackedUp() does not change package state`() { - var version = Random.nextLong(Long.MAX_VALUE) - var packageMetadata = PackageMetadata( - version = version, - installer = getRandomString(), - signatures = listOf("sig") - ) - - every { context.packageManager } returns packageManager - expectReadFromCache() - expectWriteToCache(initialMetadata) - val oldState = UNKNOWN_ERROR - - // state doesn't change for APK_AND_DATA - packageMetadata = packageMetadata.copy(version = ++version, state = APK_AND_DATA) - manager.onApkBackedUp(packageInfo, packageMetadata) - assertEquals( - packageMetadata.copy(state = oldState), - manager.getPackageMetadata(packageName) - ) - - // state doesn't change for QUOTA_EXCEEDED - packageMetadata = packageMetadata.copy(version = ++version, state = QUOTA_EXCEEDED) - manager.onApkBackedUp(packageInfo, packageMetadata) - assertEquals( - packageMetadata.copy(state = oldState), - manager.getPackageMetadata(packageName) - ) - - // state doesn't change for NO_DATA - packageMetadata = packageMetadata.copy(version = ++version, state = NO_DATA) - manager.onApkBackedUp(packageInfo, packageMetadata) - assertEquals( - packageMetadata.copy(state = oldState), - manager.getPackageMetadata(packageName) - ) - - // state doesn't change for NOT_ALLOWED - packageMetadata = packageMetadata.copy(version = ++version, state = NOT_ALLOWED) - manager.onApkBackedUp(packageInfo, packageMetadata) - assertEquals( - packageMetadata.copy(state = oldState), - manager.getPackageMetadata(packageName) - ) - - // state doesn't change for WAS_STOPPED - packageMetadata = packageMetadata.copy(version = ++version, state = WAS_STOPPED) - manager.onApkBackedUp(packageInfo, packageMetadata) - assertEquals( - packageMetadata.copy(state = oldState), - manager.getPackageMetadata(packageName) - ) - - verify { - cacheInputStream.close() - cacheOutputStream.close() - } - } - - @Test - fun `test onApkBackedUp() throws while writing local cache`() { - val packageMetadata = PackageMetadata( - time = 0L, - version = Random.nextLong(Long.MAX_VALUE), - installer = getRandomString(), - signatures = listOf("sig") - ) - - every { context.packageManager } returns packageManager - expectReadFromCache() - - assertNull(manager.getPackageMetadata(packageName)) - - every { metadataWriter.encode(initialMetadata) } returns encodedMetadata - every { - context.openFileOutput( - METADATA_CACHE_FILE, - MODE_PRIVATE - ) - } throws FileNotFoundException() - - assertThrows { - manager.onApkBackedUp(packageInfo, packageMetadata) - } - - // metadata change got reverted - assertNull(manager.getPackageMetadata(packageName)) - - verify { - cacheInputStream.close() - } - } - @Test fun `test onPackageBackedUp()`() { packageInfo.applicationInfo!!.flags = FLAG_SYSTEM @@ -300,7 +106,7 @@ class MetadataManagerTest { every { packageService.launchableSystemApps } returns emptyList() expectReadFromCache() every { clock.time() } returns time - expectModifyMetadata(initialMetadata) + expectWriteToCache(initialMetadata) manager.onPackageBackedUp(packageInfo, BackupType.FULL, size) @@ -314,7 +120,6 @@ class MetadataManagerTest { ), manager.getPackageMetadata(packageName) ) - assertEquals(time, manager.getLastBackupTime()) assertFalse(updatedMetadata.d2dBackup) verify { @@ -323,34 +128,16 @@ class MetadataManagerTest { } } - @Test - fun `test onPackageBackedUp() with D2D enabled`() { - expectReadFromCache() - every { clock.time() } returns time - expectModifyMetadata(initialMetadata) - - every { settingsManager.d2dBackupsEnabled() } returns true - every { context.packageManager } returns packageManager - - manager.onPackageBackedUp(packageInfo, BackupType.FULL, 0L) - assertTrue(initialMetadata.d2dBackup) - - verify { - cacheInputStream.close() - cacheOutputStream.close() - } - } - @Test fun `test onPackageBackedUp() with filled cache`() { val cachedPackageName = getRandomString() val cacheTime = time - 1 - val cachedMetadata = initialMetadata.copy(time = cacheTime) + val cachedMetadata = initialMetadata.copy() cachedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(cacheTime) cachedMetadata.packageMetadataMap[packageName] = PackageMetadata(cacheTime) - val updatedMetadata = cachedMetadata.copy(time = time) + val updatedMetadata = cachedMetadata.copy() updatedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(time) updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(time, state = APK_AND_DATA) @@ -358,11 +145,10 @@ class MetadataManagerTest { expectReadFromCache() every { context.packageManager } returns packageManager every { clock.time() } returns time - expectModifyMetadata(updatedMetadata) + expectWriteToCache(updatedMetadata) manager.onPackageBackedUp(packageInfo, BackupType.FULL, 0L) - assertEquals(time, manager.getLastBackupTime()) assertEquals(PackageMetadata(time), manager.getPackageMetadata(cachedPackageName)) assertEquals( updatedMetadata.packageMetadataMap[packageName], @@ -416,7 +202,7 @@ class MetadataManagerTest { updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(state = NO_DATA) expectReadFromCache() - expectModifyMetadata(updatedMetadata) + expectWriteToCache(updatedMetadata) manager.onPackageBackupError(packageInfo, NO_DATA, BackupType.KV) } @@ -429,34 +215,11 @@ class MetadataManagerTest { every { context.packageManager } returns packageManager expectReadFromCache() - expectModifyMetadata(updatedMetadata) + expectWriteToCache(updatedMetadata) manager.onPackageBackupError(packageInfo, WAS_STOPPED) } - @Test - fun `test getLastBackupTime() on first run`() { - every { context.openFileInput(METADATA_CACHE_FILE) } throws FileNotFoundException() - - assertEquals(0L, manager.getLastBackupTime()) - } - - @Test - fun `test getLastBackupTime() and getBackupToken() with cached metadata`() { - initialMetadata.time = Random.nextLong() - - expectReadFromCache() - - assertEquals(initialMetadata.time, manager.getLastBackupTime()) - - verify { cacheInputStream.close() } - } - - private fun expectModifyMetadata(metadata: BackupMetadata) { - every { metadataWriter.write(metadata, storageOutputStream) } just Runs - expectWriteToCache(metadata) - } - private fun expectReadFromCache() { val byteArray = ByteArray(DEFAULT_BUFFER_SIZE) every { context.openFileInput(METADATA_CACHE_FILE) } returns cacheInputStream diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReadWriteTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReadWriteTest.kt deleted file mode 100644 index 66df0b9b..00000000 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReadWriteTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 The Calyx Institute - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.stevesoltys.seedvault.metadata - -import android.content.Context -import com.stevesoltys.seedvault.crypto.CipherFactoryImpl -import com.stevesoltys.seedvault.crypto.CryptoImpl -import com.stevesoltys.seedvault.crypto.KEY_SIZE_BYTES -import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl -import com.stevesoltys.seedvault.getRandomBase64 -import com.stevesoltys.seedvault.getRandomString -import com.stevesoltys.seedvault.header.HeaderReaderImpl -import com.stevesoltys.seedvault.header.VERSION -import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA -import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED -import io.mockk.mockk -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import javax.crypto.spec.SecretKeySpec -import kotlin.random.Random - -@TestInstance(PER_CLASS) -internal class MetadataReadWriteTest { - - private val secretKey = SecretKeySpec( - "This is a legacy backup key 1234".toByteArray(), 0, KEY_SIZE_BYTES, "AES" - ) - private val context = mockk() - private val keyManager = KeyManagerTestImpl(secretKey) - private val cipherFactory = CipherFactoryImpl(keyManager) - private val headerReader = HeaderReaderImpl() - private val cryptoImpl = CryptoImpl(context, keyManager, cipherFactory, headerReader) - - private val writer = MetadataWriterImpl(cryptoImpl) - private val reader = MetadataReaderImpl(cryptoImpl) - - private val packages = HashMap().apply { - put(getRandomString(), PackageMetadata(Random.nextLong(), APK_AND_DATA, BackupType.FULL)) - put(getRandomString(), PackageMetadata(Random.nextLong(), WAS_STOPPED, BackupType.KV)) - } - - @Test - fun `written metadata matches read metadata`() { - val metadata = getMetadata(packages) - val outputStream = ByteArrayOutputStream() - - writer.write(metadata, outputStream) - - val inputStream = ByteArrayInputStream(outputStream.toByteArray()) - - assertEquals(metadata, reader.readMetadata(inputStream, metadata.token)) - } - - private fun getMetadata( - packageMetadata: HashMap = HashMap(), - ): BackupMetadata { - return BackupMetadata( - version = VERSION, - token = Random.nextLong(), - salt = getRandomBase64(32), - time = Random.nextLong(), - androidVersion = Random.nextInt(), - androidIncremental = getRandomString(), - deviceName = getRandomString(), - packageMetadataMap = packageMetadata - ) - } - -} 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 30b7fbb8..98030773 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReaderTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReaderTest.kt @@ -9,16 +9,10 @@ import com.stevesoltys.seedvault.Utf8 import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.getRandomBase64 import com.stevesoltys.seedvault.getRandomString -import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED -import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import io.mockk.mockk import org.json.JSONArray import org.json.JSONObject -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertThrows -import org.junit.jupiter.api.Assertions.fail import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS @@ -29,7 +23,7 @@ class MetadataReaderTest { private val crypto = mockk() - private val encoder = MetadataWriterImpl(crypto) + private val encoder = MetadataWriterImpl() private val decoder = MetadataReaderImpl(crypto) private val metadata = getMetadata() @@ -49,11 +43,6 @@ class MetadataReaderTest { } } - @Test - fun `expected version and token do not throw SecurityException`() { - decoder.decode(metadataByteArray, metadata.version, metadata.token) - } - @Test fun `malformed JSON throws SecurityException`() { assertThrows(SecurityException::class.java) { @@ -61,22 +50,6 @@ class MetadataReaderTest { } } - @Test - fun `missing fields throws SecurityException`() { - val json = JSONObject().apply { - put(JSON_METADATA, JSONObject().apply { - put(JSON_METADATA_VERSION, metadata.version.toInt()) - put(JSON_METADATA_TOKEN, metadata.token) - put(JSON_METADATA_SDK_INT, metadata.androidVersion) - }) - } - val jsonBytes = json.toString().toByteArray(Utf8) - - assertThrows(SecurityException::class.java) { - decoder.decode(jsonBytes, metadata.version, metadata.token) - } - } - @Test fun `missing meta throws SecurityException`() { val json = JSONObject().apply { @@ -89,26 +62,6 @@ class MetadataReaderTest { } } - @Test - fun `package metadata gets read`() { - val packageMetadata = HashMap().apply { - put( - "org.example", PackageMetadata( - time = Random.nextLong(), - state = QUOTA_EXCEEDED, - backupType = BackupType.FULL, - version = Random.nextLong(), - installer = getRandomString(), - sha256 = getRandomString(), - signatures = listOf(getRandomString(), getRandomString()) - ) - ) - } - val metadata = getMetadata(packageMetadata) - val metadataByteArray = encoder.encode(metadata) - decoder.decode(metadataByteArray, metadata.version, metadata.token) - } - @Test fun `package metadata with missing time throws`() { val json = JSONObject(metadataByteArray.toString(Utf8)) @@ -124,55 +77,6 @@ class MetadataReaderTest { } } - @Test - fun `package metadata unknown state gets mapped to error`() { - val json = JSONObject(metadataByteArray.toString(Utf8)) - json.put("org.example", JSONObject().apply { - put(JSON_PACKAGE_TIME, Random.nextLong()) - put(JSON_PACKAGE_STATE, getRandomString()) - put(JSON_PACKAGE_BACKUP_TYPE, BackupType.FULL.name) - put(JSON_PACKAGE_VERSION, Random.nextLong()) - put(JSON_PACKAGE_INSTALLER, getRandomString()) - put(JSON_PACKAGE_SHA256, getRandomString()) - put(JSON_PACKAGE_SIGNATURES, JSONArray(listOf(getRandomString(), getRandomString()))) - }) - val jsonBytes = json.toString().toByteArray(Utf8) - val metadata = decoder.decode(jsonBytes, metadata.version, metadata.token) - assertEquals(this.metadata.salt, metadata.salt) - assertEquals(UNKNOWN_ERROR, metadata.packageMetadataMap["org.example"]!!.state) - assertEquals(BackupType.FULL, metadata.packageMetadataMap["org.example"]!!.backupType) - } - - @Test - fun `package metadata missing system gets mapped to false`() { - val json = JSONObject(metadataByteArray.toString(Utf8)) - json.put("org.example", JSONObject().apply { - put(JSON_PACKAGE_TIME, Random.nextLong()) - }) - val jsonBytes = json.toString().toByteArray(Utf8) - val metadata = decoder.decode(jsonBytes, metadata.version, metadata.token) - assertFalse(metadata.packageMetadataMap["org.example"]!!.system) - assertNull(metadata.packageMetadataMap["org.example"]!!.backupType) - } - - @Test - fun `package metadata can only include time`() { - val json = JSONObject(metadataByteArray.toString(Utf8)) - json.put("org.example", JSONObject().apply { - put(JSON_PACKAGE_TIME, Random.nextLong()) - put(JSON_PACKAGE_BACKUP_TYPE, BackupType.KV.name) - }) - val jsonBytes = json.toString().toByteArray(Utf8) - val result = decoder.decode(jsonBytes, metadata.version, metadata.token) - - assertEquals(1, result.packageMetadataMap.size) - val packageMetadata = result.packageMetadataMap.getOrElse("org.example") { fail() } - assertEquals(BackupType.KV, packageMetadata.backupType) - assertNull(packageMetadata.version) - assertNull(packageMetadata.installer) - assertNull(packageMetadata.signatures) - } - private fun getMetadata( packageMetadata: PackageMetadataMap = PackageMetadataMap(), ): BackupMetadata { diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt deleted file mode 100644 index b1b1cb99..00000000 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt +++ /dev/null @@ -1,161 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 The Calyx Institute - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.stevesoltys.seedvault.metadata - -import com.stevesoltys.seedvault.crypto.Crypto -import com.stevesoltys.seedvault.getRandomBase64 -import com.stevesoltys.seedvault.getRandomString -import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA -import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED -import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA -import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED -import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED -import io.mockk.mockk -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS -import kotlin.random.Random -import kotlin.random.nextLong - -@TestInstance(PER_CLASS) -internal class MetadataWriterDecoderTest { - - private val crypto = mockk() - - private val encoder = MetadataWriterImpl(crypto) - private val decoder = MetadataReaderImpl(crypto) - - @Test - fun `encoded metadata matches decoded metadata (no packages)`() { - val metadata = getMetadata().let { - if (it.version == 0.toByte()) it.copy(salt = "") // no salt in version 0 - else it - } - assertEquals( - metadata, - decoder.decode(encoder.encode(metadata), metadata.version, metadata.token) - ) - } - - @Test - fun `encoded metadata matches decoded metadata (with package, no apk info)`() { - val time = Random.nextLong() - val packages = HashMap().apply { - put(getRandomString(), PackageMetadata(time, APK_AND_DATA, BackupType.FULL)) - put(getRandomString(), PackageMetadata(time, WAS_STOPPED, BackupType.KV)) - } - val metadata = getMetadata(packages) - assertEquals( - metadata, - decoder.decode(encoder.encode(metadata), metadata.version, metadata.token) - ) - } - - @Test - fun `encoded metadata matches decoded metadata (full package)`() { - val packages = HashMap().apply { - put( - getRandomString(), PackageMetadata( - time = Random.nextLong(), - state = APK_AND_DATA, - backupType = BackupType.FULL, - size = Random.nextLong(0, Long.MAX_VALUE), - name = getRandomString(), - system = Random.nextBoolean(), - isLaunchableSystemApp = Random.nextBoolean(), - version = Random.nextLong(), - installer = getRandomString(), - splits = listOf( - ApkSplit(getRandomString(), null, getRandomString()), - ApkSplit(getRandomString(), 0L, getRandomString()), - ApkSplit( - name = getRandomString(), - size = Random.nextLong(0, Long.MAX_VALUE), - sha256 = getRandomString(), - ), - ), - sha256 = getRandomString(), - signatures = listOf(getRandomString(), getRandomString()) - ) - ) - } - val metadata = getMetadata(packages) - assertEquals( - metadata, - decoder.decode(encoder.encode(metadata), metadata.version, metadata.token) - ) - } - - @Test - fun `encoded metadata matches decoded metadata (three full packages)`() { - val packages = HashMap().apply { - put( - getRandomString(), PackageMetadata( - time = Random.nextLong(), - state = QUOTA_EXCEEDED, - backupType = BackupType.FULL, - name = null, - size = Random.nextLong(0..Long.MAX_VALUE), - system = Random.nextBoolean(), - version = Random.nextLong(), - installer = getRandomString(), - sha256 = getRandomString(), - signatures = listOf(getRandomString()), - ) - ) - put( - getRandomString(), PackageMetadata( - time = Random.nextLong(), - state = NO_DATA, - backupType = BackupType.KV, - size = null, - name = getRandomString(), - system = Random.nextBoolean(), - version = Random.nextLong(), - installer = getRandomString(), - sha256 = getRandomString(), - signatures = listOf(getRandomString(), getRandomString()), - ) - ) - put( - getRandomString(), PackageMetadata( - time = 0L, - state = NOT_ALLOWED, - size = 0, - system = Random.nextBoolean(), - isLaunchableSystemApp = Random.nextBoolean(), - version = Random.nextLong(), - installer = getRandomString(), - sha256 = getRandomString(), - signatures = listOf(getRandomString(), getRandomString()), - ) - ) - } - val metadata = getMetadata(packages) - assertEquals( - metadata, - decoder.decode(encoder.encode(metadata), metadata.version, metadata.token) - ) - } - - private fun getMetadata( - packageMetadata: HashMap = HashMap(), - ): BackupMetadata { - val version = Random.nextBytes(1)[0] - return BackupMetadata( - version = version, - token = Random.nextLong(), - salt = if (version != 0.toByte()) getRandomBase64(32) else "", - time = Random.nextLong(), - androidVersion = Random.nextInt(), - androidIncremental = getRandomString(), - deviceName = getRandomString(), - packageMetadataMap = packageMetadata - ) - } - -} 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 21e8d4c2..84346b3b 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt @@ -147,7 +147,6 @@ internal class CoordinatorIntegrationTest : TransportTest() { val inputStream = CapturingSlot() val bOutputStream = ByteArrayOutputStream() - every { metadataManager.requiresInit } returns false every { backupReceiver.assertFinalized() } just Runs // read one key/value record and write it to output stream every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput @@ -217,7 +216,6 @@ internal class CoordinatorIntegrationTest : TransportTest() { val appData = ByteArray(size).apply { Random.nextBytes(this) } val bOutputStream = ByteArrayOutputStream() - every { metadataManager.requiresInit } returns false every { backupReceiver.assertFinalized() } just Runs // read one key/value record and write it to output stream every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput 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 3424355e..f3d2086b 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 @@ -5,14 +5,11 @@ package com.stevesoltys.seedvault.transport.backup -import android.app.backup.BackupTransport.TRANSPORT_NOT_INITIALIZED import android.app.backup.BackupTransport.TRANSPORT_OK import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED -import android.content.pm.PackageInfo import android.net.Uri import android.os.ParcelFileDescriptor -import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.backend.BackendManager import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.metadata.BackupType @@ -83,22 +80,6 @@ internal class BackupCoordinatorTest : BackupTest() { assertEquals(TRANSPORT_OK, backup.finishBackup()) } - @Test - fun `performIncrementalBackup of @pm@ causes re-init when legacy format`() = runBlocking { - val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER } - - every { backendManager.canDoBackupNow() } returns true - every { metadataManager.requiresInit } returns true - - every { data.close() } just Runs - - // returns TRANSPORT_NOT_INITIALIZED to re-init next time - assertEquals( - TRANSPORT_NOT_INITIALIZED, - backup.performIncrementalBackup(packageInfo, data, 0) - ) - } - @Test fun `getBackupQuota() delegates to right plugin`() = runBlocking { val isFullBackup = Random.nextBoolean() @@ -199,7 +180,6 @@ internal class BackupCoordinatorTest : BackupTest() { coEvery { full.performFullBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK - expectApkBackupAndMetadataWrite() every { full.quota } returns DEFAULT_QUOTA_FULL_BACKUP every { full.checkFullBackupSize(DEFAULT_QUOTA_FULL_BACKUP + 1) @@ -245,7 +225,6 @@ internal class BackupCoordinatorTest : BackupTest() { coEvery { full.performFullBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK - expectApkBackupAndMetadataWrite() every { full.quota } returns DEFAULT_QUOTA_FULL_BACKUP every { full.checkFullBackupSize(0) } returns TRANSPORT_PACKAGE_REJECTED every { full.currentPackageInfo } returns packageInfo @@ -285,9 +264,4 @@ internal class BackupCoordinatorTest : BackupTest() { fun `not allowed apps get their APKs backed up after @pm@ backup`() = runBlocking { } - private fun expectApkBackupAndMetadataWrite() { - coEvery { apkBackup.backupApkIfNecessary(packageInfo, snapshot) } just Runs - every { metadataManager.onApkBackedUp(any(), packageMetadata) } just Runs - } - } diff --git a/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt index 5d9773bb..bc1f3640 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt @@ -207,7 +207,6 @@ internal class ApkBackupManagerTest : TransportTest() { } just Runs // was backed up, get new packageMetadata coEvery { apkBackup.backupApkIfNecessary(notAllowedPackages[1], snapshot) } just Runs - every { metadataManager.onApkBackedUp(notAllowedPackages[1], packageMetadata) } just Runs every { nm.onApkBackupDone() } just Runs