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