diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderBackupPlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderBackupPlugin.kt index 72f2a86a..bd9a0fba 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderBackupPlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderBackupPlugin.kt @@ -11,9 +11,11 @@ import java.io.OutputStream private const val MIME_TYPE_APK = "application/vnd.android.package-archive" +@Suppress("BlockingMethodInNonBlockingContext") internal class DocumentsProviderBackupPlugin( - private val context: Context, - private val storage: DocumentsStorage) : BackupPlugin { + private val context: Context, + private val storage: DocumentsStorage +) : BackupPlugin { private val packageManager: PackageManager = context.packageManager @@ -41,7 +43,7 @@ internal class DocumentsProviderBackupPlugin( val fullDir = storage.currentFullBackupDir // wipe existing data - storage.getSetDir()?.findFile(FILE_BACKUP_METADATA)?.delete() + storage.getSetDir()?.findFileBlocking(context, FILE_BACKUP_METADATA)?.delete() kvDir?.deleteContents() fullDir?.deleteContents() diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderFullBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderFullBackup.kt index 70e0499f..e2bef69e 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderFullBackup.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderFullBackup.kt @@ -10,24 +10,27 @@ import java.io.OutputStream private val TAG = DocumentsProviderFullBackup::class.java.simpleName +@Suppress("BlockingMethodInNonBlockingContext") internal class DocumentsProviderFullBackup( - private val storage: DocumentsStorage, - private val context: Context) : FullBackupPlugin { + private val storage: DocumentsStorage, + private val context: Context +) : FullBackupPlugin { override fun getQuota() = DEFAULT_QUOTA_FULL_BACKUP @Throws(IOException::class) override suspend fun getOutputStream(targetPackage: PackageInfo): OutputStream { val file = storage.currentFullBackupDir?.createOrGetFile(context, targetPackage.packageName) - ?: throw IOException() + ?: throw IOException() return storage.getOutputStream(file) } @Throws(IOException::class) - override fun removeDataOfPackage(packageInfo: PackageInfo) { + override suspend fun removeDataOfPackage(packageInfo: PackageInfo) { val packageName = packageInfo.packageName Log.i(TAG, "Deleting $packageName...") - val file = storage.currentFullBackupDir?.findFile(packageName) ?: return + val file = storage.currentFullBackupDir?.findFileBlocking(context, packageName) + ?: return if (!file.delete()) throw IOException("Failed to delete $packageName") } diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderFullRestorePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderFullRestorePlugin.kt index 61b815be..2eef1943 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderFullRestorePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderFullRestorePlugin.kt @@ -1,23 +1,31 @@ package com.stevesoltys.seedvault.plugins.saf +import android.content.Context import android.content.pm.PackageInfo import com.stevesoltys.seedvault.transport.restore.FullRestorePlugin import java.io.IOException import java.io.InputStream +@Suppress("BlockingMethodInNonBlockingContext") internal class DocumentsProviderFullRestorePlugin( - private val documentsStorage: DocumentsStorage) : FullRestorePlugin { + private val context: Context, + private val documentsStorage: DocumentsStorage +) : FullRestorePlugin { @Throws(IOException::class) override suspend fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean { val backupDir = documentsStorage.getFullBackupDir(token) ?: return false - return backupDir.findFile(packageInfo.packageName) != null + return backupDir.findFileBlocking(context, packageInfo.packageName) != null } @Throws(IOException::class) - override suspend fun getInputStreamForPackage(token: Long, packageInfo: PackageInfo): InputStream { + override suspend fun getInputStreamForPackage( + token: Long, + packageInfo: PackageInfo + ): InputStream { val backupDir = documentsStorage.getFullBackupDir(token) ?: throw IOException() - val packageFile = backupDir.findFile(packageInfo.packageName) ?: throw IOException() + val packageFile = + backupDir.findFileBlocking(context, packageInfo.packageName) ?: throw IOException() return documentsStorage.getInputStream(packageFile) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderKVBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderKVBackup.kt index 7e4ad775..bc0192da 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderKVBackup.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderKVBackup.kt @@ -8,9 +8,10 @@ import com.stevesoltys.seedvault.transport.backup.KVBackupPlugin import java.io.IOException import java.io.OutputStream +@Suppress("BlockingMethodInNonBlockingContext") internal class DocumentsProviderKVBackup( - private val storage: DocumentsStorage, - private val context: Context + private val storage: DocumentsStorage, + private val context: Context ) : KVBackupPlugin { private var packageFile: DocumentFile? = null @@ -18,8 +19,9 @@ internal class DocumentsProviderKVBackup( override fun getQuota(): Long = DEFAULT_QUOTA_KEY_VALUE_BACKUP @Throws(IOException::class) - override fun hasDataForPackage(packageInfo: PackageInfo): Boolean { - val packageFile = storage.currentKvBackupDir?.findFile(packageInfo.packageName) + override suspend fun hasDataForPackage(packageInfo: PackageInfo): Boolean { + val packageFile = + storage.currentKvBackupDir?.findFileBlocking(context, packageInfo.packageName) ?: return false return packageFile.listFiles().isNotEmpty() } @@ -27,27 +29,31 @@ internal class DocumentsProviderKVBackup( @Throws(IOException::class) override suspend fun ensureRecordStorageForPackage(packageInfo: PackageInfo) { // remember package file for subsequent operations - packageFile = storage.getOrCreateKVBackupDir().createOrGetDirectory(context, packageInfo.packageName) + packageFile = + storage.getOrCreateKVBackupDir().createOrGetDirectory(context, packageInfo.packageName) } @Throws(IOException::class) - override fun removeDataOfPackage(packageInfo: PackageInfo) { + override suspend fun removeDataOfPackage(packageInfo: PackageInfo) { // we cannot use the cached this.packageFile here, // because this can be called before [ensureRecordStorageForPackage] - val packageFile = storage.currentKvBackupDir?.findFile(packageInfo.packageName) ?: return + val packageFile = storage.currentKvBackupDir?.findFileBlocking(context, packageInfo.packageName) ?: return packageFile.delete() } @Throws(IOException::class) - override fun deleteRecord(packageInfo: PackageInfo, key: String) { + override suspend fun deleteRecord(packageInfo: PackageInfo, key: String) { val packageFile = this.packageFile ?: throw AssertionError() packageFile.assertRightFile(packageInfo) - val keyFile = packageFile.findFile(key) ?: return + val keyFile = packageFile.findFileBlocking(context, key) ?: return keyFile.delete() } @Throws(IOException::class) - override suspend fun getOutputStreamForRecord(packageInfo: PackageInfo, key: String): OutputStream { + override suspend fun getOutputStreamForRecord( + packageInfo: PackageInfo, + key: String + ): OutputStream { val packageFile = this.packageFile ?: throw AssertionError() packageFile.assertRightFile(packageInfo) val keyFile = packageFile.createOrGetFile(context, key) diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderKVRestorePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderKVRestorePlugin.kt index 47253160..d62f6952 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderKVRestorePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderKVRestorePlugin.kt @@ -1,12 +1,17 @@ package com.stevesoltys.seedvault.plugins.saf +import android.content.Context import android.content.pm.PackageInfo import androidx.documentfile.provider.DocumentFile import com.stevesoltys.seedvault.transport.restore.KVRestorePlugin import java.io.IOException import java.io.InputStream -internal class DocumentsProviderKVRestorePlugin(private val storage: DocumentsStorage) : KVRestorePlugin { +@Suppress("BlockingMethodInNonBlockingContext") +internal class DocumentsProviderKVRestorePlugin( + private val context: Context, + private val storage: DocumentsStorage +) : KVRestorePlugin { private var packageDir: DocumentFile? = null @@ -14,7 +19,7 @@ internal class DocumentsProviderKVRestorePlugin(private val storage: DocumentsSt return try { val backupDir = storage.getKVBackupDir(token) ?: return false // remember package file for subsequent operations - packageDir = backupDir.findFile(packageInfo.packageName) + packageDir = backupDir.findFileBlocking(context, packageInfo.packageName) packageDir != null } catch (e: IOException) { false @@ -25,15 +30,19 @@ internal class DocumentsProviderKVRestorePlugin(private val storage: DocumentsSt val packageDir = this.packageDir ?: throw AssertionError() packageDir.assertRightFile(packageInfo) return packageDir.listFiles() - .filter { file -> file.name != null } - .map { file -> file.name!! } + .filter { file -> file.name != null } + .map { file -> file.name!! } } @Throws(IOException::class) - override fun getInputStreamForRecord(token: Long, packageInfo: PackageInfo, key: String): InputStream { + override suspend fun getInputStreamForRecord( + token: Long, + packageInfo: PackageInfo, + key: String + ): InputStream { val packageDir = this.packageDir ?: throw AssertionError() packageDir.assertRightFile(packageInfo) - val keyFile = packageDir.findFile(key) ?: throw IOException() + val keyFile = packageDir.findFileBlocking(context, key) ?: throw IOException() return storage.getInputStream(keyFile) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderRestorePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderRestorePlugin.kt index 98050d2d..4da189d4 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderRestorePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderRestorePlugin.kt @@ -16,17 +16,18 @@ import java.io.InputStream private val TAG = DocumentsProviderRestorePlugin::class.java.simpleName @WorkerThread -@Suppress("BlockingMethodInNonBlockingContext") // all methods do I/O +@Suppress("BlockingMethodInNonBlockingContext") // all methods do I/O internal class DocumentsProviderRestorePlugin( - private val context: Context, - private val storage: DocumentsStorage) : RestorePlugin { + private val context: Context, + private val storage: DocumentsStorage +) : RestorePlugin { override val kvRestorePlugin: KVRestorePlugin by lazy { - DocumentsProviderKVRestorePlugin(storage) + DocumentsProviderKVRestorePlugin(context, storage) } override val fullRestorePlugin: FullRestorePlugin by lazy { - DocumentsProviderFullRestorePlugin(storage) + DocumentsProviderFullRestorePlugin(context, storage) } @Throws(IOException::class) @@ -42,7 +43,7 @@ internal class DocumentsProviderRestorePlugin( val backupSets = getBackups(context, rootDir) val iterator = backupSets.iterator() return generateSequence { - if (!iterator.hasNext()) return@generateSequence null // end sequence + if (!iterator.hasNext()) return@generateSequence null // end sequence val backupSet = iterator.next() try { val stream = storage.getInputStream(backupSet.metadataFile) @@ -64,18 +65,9 @@ internal class DocumentsProviderRestorePlugin( return backupSets } for (set in files) { - if (!set.isDirectory || set.name == null) { - if (set.name != FILE_NO_MEDIA) { - Log.w(TAG, "Found invalid backup set folder: ${set.name}") - } - continue - } - val token = try { - set.name!!.toLong() - } catch (e: NumberFormatException) { - Log.w(TAG, "Found invalid backup set folder: ${set.name}") - continue - } + // get current token from set or continue to next file/set + val token = set.getTokenOrNull() ?: continue + // block until children of set are available val metadata = try { set.findFileBlocking(context, FILE_BACKUP_METADATA) @@ -92,10 +84,26 @@ internal class DocumentsProviderRestorePlugin( return backupSets } + private fun DocumentFile.getTokenOrNull(): Long? { + if (!isDirectory || name == null) { + if (name != FILE_NO_MEDIA) { + Log.w(TAG, "Found invalid backup set folder: $name") + } + return null + } + return try { + name!!.toLong() + } catch (e: NumberFormatException) { + Log.w(TAG, "Found invalid backup set folder: $name") + null + } + } + @Throws(IOException::class) override suspend fun getApkInputStream(token: Long, packageName: String): InputStream { val setDir = storage.getSetDir(token) ?: throw IOException() - val file = setDir.findFile("$packageName.apk") ?: throw FileNotFoundException() + val file = + setDir.findFileBlocking(context, "$packageName.apk") ?: throw FileNotFoundException() return storage.getInputStream(file) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt index d300efe3..cdc80a71 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt @@ -41,9 +41,9 @@ private const val MIME_TYPE = "application/octet-stream" private val TAG = DocumentsStorage::class.java.simpleName internal class DocumentsStorage( - private val context: Context, - private val metadataManager: MetadataManager, - private val settingsManager: SettingsManager + private val context: Context, + private val metadataManager: MetadataManager, + private val settingsManager: SettingsManager ) { private val contentResolver = context.contentResolver @@ -58,7 +58,7 @@ internal class DocumentsStorage( get() = runBlocking { if (field == null) { val parent = storage?.getDocumentFile(context) - ?: return@runBlocking null + ?: return@runBlocking null field = try { parent.createOrGetDirectory(context, DIRECTORY_ROOT).apply { // create .nomedia file to prevent Android's MediaScanner @@ -180,7 +180,11 @@ internal class DocumentsStorage( * If we were trying to create it right away, some providers create "filename (1)". */ @Throws(IOException::class) -internal suspend fun DocumentFile.createOrGetFile(context: Context, name: String, mimeType: String = MIME_TYPE): DocumentFile { +internal suspend fun DocumentFile.createOrGetFile( + context: Context, + name: String, + mimeType: String = MIME_TYPE +): DocumentFile { return findFileBlocking(context, name) ?: createFile(mimeType, name)?.apply { check(this.name == name) { "File named ${this.name}, but should be $name" } } ?: throw IOException() @@ -276,25 +280,26 @@ suspend fun DocumentFile.findFileBlocking(context: Context, displayName: String) */ @VisibleForTesting @Throws(IOException::class, TimeoutCancellationException::class) -internal suspend fun getLoadedCursor(timeout: Long = 15_000, query: () -> Cursor?) = withTimeout(timeout) { - suspendCancellableCoroutine { cont -> - val cursor = query() ?: throw IOException() - cont.invokeOnCancellation { closeQuietly(cursor) } - val loading = cursor.extras.getBoolean(EXTRA_LOADING, false) - if (loading) { - Log.d(TAG, "Wait for children to get loaded...") - cursor.registerContentObserver(object : ContentObserver(null) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - Log.d(TAG, "Children loaded. Continue...") - closeQuietly(cursor) - val newCursor = query() - if (newCursor == null) cont.cancel(IOException("query returned no results")) - else cont.resume(newCursor) - } - }) - } else { - // not loading, return cursor right away - cont.resume(cursor) +internal suspend fun getLoadedCursor(timeout: Long = 15_000, query: () -> Cursor?) = + withTimeout(timeout) { + suspendCancellableCoroutine { cont -> + val cursor = query() ?: throw IOException() + cont.invokeOnCancellation { closeQuietly(cursor) } + val loading = cursor.extras.getBoolean(EXTRA_LOADING, false) + if (loading) { + Log.d(TAG, "Wait for children to get loaded...") + cursor.registerContentObserver(object : ContentObserver(null) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + Log.d(TAG, "Children loaded. Continue...") + closeQuietly(cursor) + val newCursor = query() + if (newCursor == null) cont.cancel(IOException("query returned no results")) + else cont.resume(newCursor) + } + }) + } else { + // not loading, return cursor right away + cont.resume(cursor) + } } } -} diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt index 490bb8d4..cd32d69c 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt @@ -18,14 +18,16 @@ import org.koin.core.inject val TRANSPORT_ID: String = ConfigurableBackupTransport::class.java.name -private const val TRANSPORT_DIRECTORY_NAME = "com.stevesoltys.seedvault.transport.ConfigurableBackupTransport" +private const val TRANSPORT_DIRECTORY_NAME = + "com.stevesoltys.seedvault.transport.ConfigurableBackupTransport" private val TAG = ConfigurableBackupTransport::class.java.simpleName /** * @author Steve Soltys * @author Torsten Grote */ -class ConfigurableBackupTransport internal constructor(private val context: Context) : BackupTransport(), KoinComponent { +class ConfigurableBackupTransport internal constructor(private val context: Context) : + BackupTransport(), KoinComponent { private val backupCoordinator by inject() private val restoreCoordinator by inject() @@ -62,7 +64,10 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont backupCoordinator.initializeDevice() } - override fun isAppEligibleForBackup(targetPackage: PackageInfo, isFullBackup: Boolean): Boolean { + override fun isAppEligibleForBackup( + targetPackage: PackageInfo, + isFullBackup: Boolean + ): Boolean { return backupCoordinator.isAppEligibleForBackup(targetPackage, isFullBackup) } @@ -70,8 +75,8 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont backupCoordinator.getBackupQuota(packageName, isFullBackup) } - override fun clearBackupData(packageInfo: PackageInfo): Int { - return backupCoordinator.clearBackupData(packageInfo) + override fun clearBackupData(packageInfo: PackageInfo): Int = runBlocking { + backupCoordinator.clearBackupData(packageInfo) } override fun finishBackup(): Int = runBlocking { @@ -86,11 +91,18 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont return backupCoordinator.requestBackupTime() } - override fun performBackup(packageInfo: PackageInfo, inFd: ParcelFileDescriptor, flags: Int): Int = runBlocking { + override fun performBackup( + packageInfo: PackageInfo, + inFd: ParcelFileDescriptor, + flags: Int + ): Int = runBlocking { backupCoordinator.performIncrementalBackup(packageInfo, inFd, flags) } - override fun performBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor): Int { + override fun performBackup( + targetPackage: PackageInfo, + fileDescriptor: ParcelFileDescriptor + ): Int { Log.w(TAG, "Warning: Legacy performBackup() method called.") return performBackup(targetPackage, fileDescriptor, 0) } @@ -107,11 +119,18 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont return backupCoordinator.checkFullBackupSize(size) } - override fun performFullBackup(targetPackage: PackageInfo, socket: ParcelFileDescriptor, flags: Int): Int = runBlocking { + override fun performFullBackup( + targetPackage: PackageInfo, + socket: ParcelFileDescriptor, + flags: Int + ): Int = runBlocking { backupCoordinator.performFullBackup(targetPackage, socket, flags) } - override fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor): Int = runBlocking { + override fun performFullBackup( + targetPackage: PackageInfo, + fileDescriptor: ParcelFileDescriptor + ): Int = runBlocking { Log.w(TAG, "Warning: Legacy performFullBackup() method called.") backupCoordinator.performFullBackup(targetPackage, fileDescriptor, 0) } @@ -148,8 +167,8 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont restoreCoordinator.nextRestorePackage() } - override fun getRestoreData(outputFileDescriptor: ParcelFileDescriptor): Int { - return restoreCoordinator.getRestoreData(outputFileDescriptor) + override fun getRestoreData(outputFileDescriptor: ParcelFileDescriptor): Int = runBlocking { + restoreCoordinator.getRestoreData(outputFileDescriptor) } override fun abortFullRestore(): Int { 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 f3ff72e9..79850a72 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 @@ -33,16 +33,17 @@ private val TAG = BackupCoordinator::class.java.simpleName @WorkerThread // entire class should always be accessed from a worker thread, so blocking is ok @Suppress("BlockingMethodInNonBlockingContext") internal class BackupCoordinator( - private val context: Context, - private val plugin: BackupPlugin, - private val kv: KVBackup, - private val full: FullBackup, - private val apkBackup: ApkBackup, - private val clock: Clock, - private val packageService: PackageService, - private val metadataManager: MetadataManager, - private val settingsManager: SettingsManager, - private val nm: BackupNotificationManager) { + private val context: Context, + private val plugin: BackupPlugin, + private val kv: KVBackup, + private val full: FullBackup, + private val apkBackup: ApkBackup, + private val clock: Clock, + private val packageService: PackageService, + private val metadataManager: MetadataManager, + private val settingsManager: SettingsManager, + private val nm: BackupNotificationManager +) { private var calledInitialize = false private var calledClearBackupData = false @@ -92,7 +93,10 @@ internal class BackupCoordinator( } } - fun isAppEligibleForBackup(targetPackage: PackageInfo, @Suppress("UNUSED_PARAMETER") isFullBackup: Boolean): Boolean { + fun isAppEligibleForBackup( + targetPackage: PackageInfo, + @Suppress("UNUSED_PARAMETER") isFullBackup: Boolean + ): Boolean { val packageName = targetPackage.packageName // Check that the app is not blacklisted by the user val enabled = settingsManager.isBackupEnabled(packageName) @@ -142,7 +146,11 @@ internal class BackupCoordinator( Log.i(TAG, "Request incremental backup time. Returned $this") } - suspend fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int { + suspend fun performIncrementalBackup( + packageInfo: PackageInfo, + data: ParcelFileDescriptor, + flags: Int + ): Int { cancelReason = UNKNOWN_ERROR val packageName = packageInfo.packageName if (packageName == MAGIC_PACKAGE_MANAGER) { @@ -185,7 +193,11 @@ internal class BackupCoordinator( return result } - suspend fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int): Int { + suspend fun performFullBackup( + targetPackage: PackageInfo, + fileDescriptor: ParcelFileDescriptor, + flags: Int + ): Int { cancelReason = UNKNOWN_ERROR return full.performFullBackup(targetPackage, fileDescriptor, flags) } @@ -207,7 +219,7 @@ internal class BackupCoordinator( */ suspend fun cancelFullBackup() { val packageInfo = full.getCurrentPackage() - ?: throw AssertionError("Cancelling full backup, but no current package") + ?: throw AssertionError("Cancelling full backup, but no current package") Log.i(TAG, "Cancel full backup of ${packageInfo.packageName} because of $cancelReason") onPackageBackupError(packageInfo) full.cancelFullBackup() @@ -224,7 +236,7 @@ internal class BackupCoordinator( * * @return the same error codes as [performFullBackup]. */ - fun clearBackupData(packageInfo: PackageInfo): Int { + suspend fun clearBackupData(packageInfo: PackageInfo): Int { val packageName = packageInfo.packageName Log.i(TAG, "Clear Backup Data of $packageName.") try { @@ -254,12 +266,12 @@ internal class BackupCoordinator( suspend fun finishBackup(): Int = when { kv.hasState() -> { check(!full.hasState()) { "K/V backup has state, but full backup has dangling state as well" } - onPackageBackedUp(kv.getCurrentPackage()!!) // not-null because we have state + onPackageBackedUp(kv.getCurrentPackage()!!) // not-null because we have state kv.finishBackup() } full.hasState() -> { check(!kv.hasState()) { "Full backup has state, but K/V backup has dangling state as well" } - onPackageBackedUp(full.getCurrentPackage()!!) // not-null because we have state + onPackageBackedUp(full.getCurrentPackage()!!) // not-null because we have state full.finishBackup() } calledInitialize || calledClearBackupData -> { @@ -281,7 +293,10 @@ internal class BackupCoordinator( } } - private suspend fun backUpApk(packageInfo: PackageInfo, packageState: PackageState = UNKNOWN_ERROR) { + private suspend fun backUpApk( + packageInfo: PackageInfo, + packageState: PackageState = UNKNOWN_ERROR + ) { val packageName = packageInfo.packageName try { apkBackup.backupApkIfNecessary(packageInfo, packageState) { diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt index 15b84089..9dc8cf94 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt @@ -18,10 +18,11 @@ import java.io.InputStream import java.io.OutputStream private class FullBackupState( - internal val packageInfo: PackageInfo, - internal val inputFileDescriptor: ParcelFileDescriptor, - internal val inputStream: InputStream, - internal var outputStreamInit: (suspend () -> OutputStream)?) { + internal val packageInfo: PackageInfo, + internal val inputFileDescriptor: ParcelFileDescriptor, + internal val inputStream: InputStream, + internal var outputStreamInit: (suspend () -> OutputStream)? +) { internal var outputStream: OutputStream? = null internal val packageName: String = packageInfo.packageName internal var size: Long = 0 @@ -33,10 +34,11 @@ private val TAG = FullBackup::class.java.simpleName @Suppress("BlockingMethodInNonBlockingContext") internal class FullBackup( - private val plugin: FullBackupPlugin, - private val inputFactory: InputFactory, - private val headerWriter: HeaderWriter, - private val crypto: Crypto) { + private val plugin: FullBackupPlugin, + private val inputFactory: InputFactory, + private val headerWriter: HeaderWriter, + private val crypto: Crypto +) { private var state: FullBackupState? = null @@ -90,7 +92,11 @@ internal class FullBackup( * [TRANSPORT_OK] to indicate that the OS may proceed with delivering backup data; * [TRANSPORT_ERROR] to indicate an error that precludes performing a backup at this time. */ - suspend fun performFullBackup(targetPackage: PackageInfo, socket: ParcelFileDescriptor, @Suppress("UNUSED_PARAMETER") flags: Int = 0): Int { + suspend fun performFullBackup( + targetPackage: PackageInfo, + socket: ParcelFileDescriptor, + @Suppress("UNUSED_PARAMETER") flags: Int = 0 + ): Int { if (state != null) throw AssertionError() Log.i(TAG, "Perform full backup for ${targetPackage.packageName}.") @@ -102,7 +108,9 @@ internal class FullBackup( val outputStream = try { plugin.getOutputStream(targetPackage) } catch (e: IOException) { - Log.e(TAG, "Error getting OutputStream for full backup of ${targetPackage.packageName}", e) + "Error getting OutputStream for full backup of ${targetPackage.packageName}".let { + Log.e(TAG, it, e) + } throw(e) } // store version header @@ -116,31 +124,36 @@ internal class FullBackup( throw(e) } outputStream - } // this lambda is only called before we actually write backup data the first time + } // this lambda is only called before we actually write backup data the first time return TRANSPORT_OK } suspend fun sendBackupData(numBytes: Int): Int { val state = this.state - ?: throw AssertionError("Attempted sendBackupData before performFullBackup") + ?: throw AssertionError("Attempted sendBackupData before performFullBackup") // check if size fits quota state.size += numBytes val quota = plugin.getQuota() if (state.size > quota) { - Log.w(TAG, "Full backup of additional $numBytes exceeds quota of $quota with ${state.size}.") + Log.w( + TAG, + "Full backup of additional $numBytes exceeds quota of $quota with ${state.size}." + ) return TRANSPORT_QUOTA_EXCEEDED } return try { // get output stream or initialize it, if it does not yet exist - check((state.outputStream != null) xor (state.outputStreamInit != null)) { "No OutputStream xor no StreamGetter" } + check((state.outputStream != null) xor (state.outputStreamInit != null)) { + "No OutputStream xor no StreamGetter" + } val outputStream = state.outputStream ?: suspend { val stream = state.outputStreamInit!!() // not-null due to check above state.outputStream = stream stream }() - state.outputStreamInit = null // the stream init lambda is not needed beyond that point + state.outputStreamInit = null // the stream init lambda is not needed beyond that point // read backup data, encrypt it and write it to output stream val payload = IOUtils.readFully(state.inputStream, numBytes) @@ -153,11 +166,11 @@ internal class FullBackup( } @Throws(IOException::class) - fun clearBackupData(packageInfo: PackageInfo) { + suspend fun clearBackupData(packageInfo: PackageInfo) { plugin.removeDataOfPackage(packageInfo) } - fun cancelFullBackup() { + suspend fun cancelFullBackup() { Log.i(TAG, "Cancel full backup") val state = this.state ?: throw AssertionError("No state when canceling") try { diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackupPlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackupPlugin.kt index e07fbff7..197de844 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackupPlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackupPlugin.kt @@ -16,6 +16,6 @@ interface FullBackupPlugin { * Remove all data associated with the given package. */ @Throws(IOException::class) - fun removeDataOfPackage(packageInfo: PackageInfo) + suspend fun removeDataOfPackage(packageInfo: PackageInfo) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt index af14e1ec..c674471d 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt @@ -23,10 +23,11 @@ private val TAG = KVBackup::class.java.simpleName @Suppress("BlockingMethodInNonBlockingContext") internal class KVBackup( - private val plugin: KVBackupPlugin, - private val inputFactory: InputFactory, - private val headerWriter: HeaderWriter, - private val crypto: Crypto) { + private val plugin: KVBackupPlugin, + private val inputFactory: InputFactory, + private val headerWriter: HeaderWriter, + private val crypto: Crypto +) { private var state: KVBackupState? = null @@ -36,7 +37,11 @@ internal class KVBackup( fun getQuota(): Long = plugin.getQuota() - suspend fun performBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int { + suspend fun performBackup( + packageInfo: PackageInfo, + data: ParcelFileDescriptor, + flags: Int + ): Int { val isIncremental = flags and FLAG_INCREMENTAL != 0 val isNonIncremental = flags and FLAG_NON_INCREMENTAL != 0 val packageName = packageInfo.packageName @@ -65,7 +70,10 @@ internal class KVBackup( return backupError(TRANSPORT_ERROR) } if (isIncremental && !hasDataForPackage) { - Log.w(TAG, "Requested incremental, but transport currently stores no data $packageName, requesting non-incremental retry.") + Log.w( + TAG, "Requested incremental, but transport currently stores no data" + + " for $packageName, requesting non-incremental retry." + ) return backupError(TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) } @@ -164,7 +172,7 @@ internal class KVBackup( } @Throws(IOException::class) - fun clearBackupData(packageInfo: PackageInfo) { + suspend fun clearBackupData(packageInfo: PackageInfo) { plugin.removeDataOfPackage(packageInfo) } @@ -179,18 +187,20 @@ internal class KVBackup( * because [finishBackup] is not called when we don't return [TRANSPORT_OK]. */ private fun backupError(result: Int): Int { - Log.i(TAG, "Resetting state because of K/V Backup error of ${state!!.packageInfo.packageName}") + "Resetting state because of K/V Backup error of ${state!!.packageInfo.packageName}".let { + Log.i(TAG, it) + } state = null return result } private class KVOperation( - internal val key: String, - internal val base64Key: String, - /** - * value is null when this is a deletion operation - */ - internal val value: ByteArray? + internal val key: String, + internal val base64Key: String, + /** + * value is null when this is a deletion operation + */ + internal val value: ByteArray? ) private sealed class Result { diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackupPlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackupPlugin.kt index 416ed978..5ec97537 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackupPlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackupPlugin.kt @@ -16,7 +16,7 @@ interface KVBackupPlugin { * Return true if there are records stored for the given package. */ @Throws(IOException::class) - fun hasDataForPackage(packageInfo: PackageInfo): Boolean + suspend fun hasDataForPackage(packageInfo: PackageInfo): Boolean /** * This marks the beginning of a backup operation. @@ -38,12 +38,12 @@ interface KVBackupPlugin { * Delete the record for the given package identified by the given key. */ @Throws(IOException::class) - fun deleteRecord(packageInfo: PackageInfo, key: String) + suspend fun deleteRecord(packageInfo: PackageInfo, key: String) /** * Remove all data associated with the given package. */ @Throws(IOException::class) - fun removeDataOfPackage(packageInfo: PackageInfo) + suspend fun removeDataOfPackage(packageInfo: PackageInfo) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt index ad0dfcc5..aee90c46 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt @@ -15,24 +15,27 @@ import com.stevesoltys.seedvault.header.HeaderReader import com.stevesoltys.seedvault.header.UnsupportedVersionException import libcore.io.IoUtils.closeQuietly import java.io.IOException -import java.util.* +import java.util.ArrayList import javax.crypto.AEADBadTagException private class KVRestoreState( - internal val token: Long, - internal val packageInfo: PackageInfo, - /** - * Optional [PackageInfo] for single package restore, optimizes restore of @pm@ - */ - internal val pmPackageInfo: PackageInfo?) + internal val token: Long, + internal val packageInfo: PackageInfo, + /** + * Optional [PackageInfo] for single package restore, optimizes restore of @pm@ + */ + internal val pmPackageInfo: PackageInfo? +) private val TAG = KVRestore::class.java.simpleName +@Suppress("BlockingMethodInNonBlockingContext") internal class KVRestore( - private val plugin: KVRestorePlugin, - private val outputFactory: OutputFactory, - private val headerReader: HeaderReader, - private val crypto: Crypto) { + private val plugin: KVRestorePlugin, + private val outputFactory: OutputFactory, + private val headerReader: HeaderReader, + private val crypto: Crypto +) { private var state: KVRestoreState? = null @@ -63,7 +66,7 @@ internal class KVRestore( * @return One of [TRANSPORT_OK] * or [TRANSPORT_ERROR] (an error occurred, the restore should be aborted and rescheduled). */ - fun getRestoreData(data: ParcelFileDescriptor): Int { + suspend fun getRestoreData(data: ParcelFileDescriptor): Int { val state = this.state ?: throw IllegalStateException("no state") // The restore set is the concatenation of the individual record blobs, @@ -122,11 +125,12 @@ internal class KVRestore( for (recordKey in records) contents.add(DecodedKey(recordKey)) // remove keys that are not needed for single package @pm@ restore val pmPackageName = state?.pmPackageInfo?.packageName - val sortedKeys = if (packageInfo.packageName == MAGIC_PACKAGE_MANAGER && pmPackageName != null) { - val keys = listOf(ANCESTRAL_RECORD_KEY, GLOBAL_METADATA_KEY, pmPackageName) - Log.d(TAG, "Single package restore, restrict restore keys to $pmPackageName") - contents.filterTo(ArrayList()) { it.key in keys } - } else contents + val sortedKeys = + if (packageInfo.packageName == MAGIC_PACKAGE_MANAGER && pmPackageName != null) { + val keys = listOf(ANCESTRAL_RECORD_KEY, GLOBAL_METADATA_KEY, pmPackageName) + Log.d(TAG, "Single package restore, restrict restore keys to $pmPackageName") + contents.filterTo(ArrayList()) { it.key in keys } + } else contents sortedKeys.sort() return sortedKeys } @@ -135,8 +139,13 @@ internal class KVRestore( * Read the encrypted value for the given key and write it to the given [BackupDataOutput]. */ @Throws(IOException::class, UnsupportedVersionException::class, SecurityException::class) - private fun readAndWriteValue(state: KVRestoreState, dKey: DecodedKey, out: BackupDataOutput) { - val inputStream = plugin.getInputStreamForRecord(state.token, state.packageInfo, dKey.base64Key) + private suspend fun readAndWriteValue( + state: KVRestoreState, + dKey: DecodedKey, + out: BackupDataOutput + ) { + val inputStream = + plugin.getInputStreamForRecord(state.token, state.packageInfo, dKey.base64Key) try { val version = headerReader.readVersion(inputStream) crypto.decryptHeader(inputStream, version, state.packageInfo.packageName, dKey.key) diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestorePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestorePlugin.kt index a79d20ea..2e0d2f7f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestorePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestorePlugin.kt @@ -29,6 +29,6 @@ interface KVRestorePlugin { * Note: Implementations might expect that you call [hasDataForPackage] before. */ @Throws(IOException::class) - fun getInputStreamForRecord(token: Long, packageInfo: PackageInfo, key: String): InputStream + suspend fun getInputStreamForRecord(token: Long, packageInfo: PackageInfo, key: String): InputStream } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt index 78100f3f..f6f0efe6 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt @@ -26,12 +26,13 @@ import libcore.io.IoUtils.closeQuietly import java.io.IOException private class RestoreCoordinatorState( - internal val token: Long, - internal val packages: Iterator, - /** - * Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@ - */ - internal val pmPackageInfo: PackageInfo?) { + internal val token: Long, + internal val packages: Iterator, + /** + * Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@ + */ + internal val pmPackageInfo: PackageInfo? +) { internal var currentPackage: String? = null } @@ -39,14 +40,15 @@ private val TAG = RestoreCoordinator::class.java.simpleName @Suppress("BlockingMethodInNonBlockingContext") internal class RestoreCoordinator( - private val context: Context, - private val settingsManager: SettingsManager, - private val metadataManager: MetadataManager, - private val notificationManager: BackupNotificationManager, - private val plugin: RestorePlugin, - private val kv: KVRestore, - private val full: FullRestore, - private val metadataReader: MetadataReader) { + private val context: Context, + private val settingsManager: SettingsManager, + private val metadataManager: MetadataManager, + private val notificationManager: BackupNotificationManager, + private val plugin: RestorePlugin, + private val kv: KVRestore, + private val full: FullRestore, + private val metadataReader: MetadataReader +) { private var state: RestoreCoordinatorState? = null private var backupMetadata: LongSparseArray? = null @@ -68,7 +70,10 @@ internal class RestoreCoordinator( "No error when getting encrypted metadata, but stream is still missing." } try { - val metadata = metadataReader.readMetadata(encryptedMetadata.inputStream, encryptedMetadata.token) + val metadata = metadataReader.readMetadata( + encryptedMetadata.inputStream, + encryptedMetadata.token + ) metadataMap.put(encryptedMetadata.token, metadata) val set = RestoreSet(metadata.deviceName, metadata.deviceName, metadata.token) restoreSets.add(set) @@ -102,7 +107,7 @@ internal class RestoreCoordinator( */ fun getCurrentRestoreSet(): Long { return metadataManager.getBackupToken() - .apply { Log.i(TAG, "Got current restore set token: $this") } + .apply { Log.i(TAG, "Got current restore set token: $this") } } /** @@ -122,22 +127,26 @@ internal class RestoreCoordinator( Log.i(TAG, "Start restore with ${packages.map { info -> info.packageName }}") // If there's only one package to restore (Auto Restore feature), add it to the state - val pmPackageInfo = if (packages.size == 2 && packages[0].packageName == MAGIC_PACKAGE_MANAGER) { - val pmPackageName = packages[1].packageName - Log.d(TAG, "Optimize for single package restore of $pmPackageName") - // check if the backup is on removable storage that is not plugged in - if (isStorageRemovableAndNotAvailable()) { - // check if we even have a backup of that app - if (metadataManager.getPackageMetadata(pmPackageName) != null) { - // remind user to plug in storage device - val storageName = settingsManager.getStorage()?.name + val pmPackageInfo = + if (packages.size == 2 && packages[0].packageName == MAGIC_PACKAGE_MANAGER) { + val pmPackageName = packages[1].packageName + Log.d(TAG, "Optimize for single package restore of $pmPackageName") + // check if the backup is on removable storage that is not plugged in + if (isStorageRemovableAndNotAvailable()) { + // check if we even have a backup of that app + if (metadataManager.getPackageMetadata(pmPackageName) != null) { + // remind user to plug in storage device + val storageName = settingsManager.getStorage()?.name ?: context.getString(R.string.settings_backup_location_none) - notificationManager.onRemovableStorageNotAvailableForRestore(pmPackageName, storageName) + notificationManager.onRemovableStorageNotAvailableForRestore( + pmPackageName, + storageName + ) + } + return TRANSPORT_ERROR } - return TRANSPORT_ERROR - } - packages[1] - } else null + packages[1] + } else null state = RestoreCoordinatorState(token, packages.iterator(), pmPackageInfo) failedPackages.clear() @@ -214,7 +223,7 @@ internal class RestoreCoordinator( * @param data An open, writable file into which the key/value backup data should be stored. * @return the same error codes as [startRestore]. */ - fun getRestoreData(data: ParcelFileDescriptor): Int { + suspend fun getRestoreData(data: ParcelFileDescriptor): Int { return kv.getRestoreData(data).apply { if (this != TRANSPORT_OK) { // add current package to failed ones 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 1b14fff7..e32ea68a 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt @@ -68,16 +68,37 @@ internal class CoordinatorIntegrationTest : TransportTest() { private val fullBackupPlugin = mockk() private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl) private val apkBackup = mockk() - private val packageService:PackageService = mockk() + private val packageService: PackageService = mockk() private val notificationManager = mockk() - private val backup = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, apkBackup, clock, packageService, metadataManager, settingsManager, notificationManager) + private val backup = BackupCoordinator( + context, + backupPlugin, + kvBackup, + fullBackup, + apkBackup, + clock, + packageService, + metadataManager, + settingsManager, + notificationManager + ) private val restorePlugin = mockk() private val kvRestorePlugin = mockk() private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl) private val fullRestorePlugin = mockk() - private val fullRestore = FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl) - private val restore = RestoreCoordinator(context, settingsManager, metadataManager, notificationManager, restorePlugin, kvRestore, fullRestore, metadataReader) + private val fullRestore = + FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl) + private val restore = RestoreCoordinator( + context, + settingsManager, + metadataManager, + notificationManager, + restorePlugin, + kvRestore, + fullRestore, + metadataReader + ) private val backupDataInput = mockk() private val fileDescriptor = mockk(relaxed = true) @@ -104,7 +125,7 @@ internal class CoordinatorIntegrationTest : TransportTest() { val bOutputStream2 = ByteArrayOutputStream() // read one key/value record and write it to output stream - every { kvBackupPlugin.hasDataForPackage(packageInfo) } returns false + coEvery { kvBackupPlugin.hasDataForPackage(packageInfo) } returns false coEvery { kvBackupPlugin.ensureRecordStorageForPackage(packageInfo) } just Runs every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput every { backupDataInput.readNextHeader() } returns true andThen true andThen false @@ -114,15 +135,37 @@ internal class CoordinatorIntegrationTest : TransportTest() { appData.copyInto(value.captured) // write the app data into the passed ByteArray appData.size } - coEvery { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key64) } returns bOutputStream + coEvery { + kvBackupPlugin.getOutputStreamForRecord( + packageInfo, + key64 + ) + } returns bOutputStream every { backupDataInput.readEntityData(capture(value2), 0, appData2.size) } answers { appData2.copyInto(value2.captured) // write the app data into the passed ByteArray appData2.size } - coEvery { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key264) } returns bOutputStream2 - coEvery { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns packageMetadata + coEvery { + kvBackupPlugin.getOutputStreamForRecord( + packageInfo, + key264 + ) + } returns bOutputStream2 + coEvery { + apkBackup.backupApkIfNecessary( + packageInfo, + UNKNOWN_ERROR, + any() + ) + } returns packageMetadata coEvery { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream - every { metadataManager.onApkBackedUp(packageInfo, packageMetadata, metadataOutputStream) } just Runs + every { + metadataManager.onApkBackedUp( + packageInfo, + packageMetadata, + metadataOutputStream + ) + } just Runs every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs // start and finish K/V backup @@ -145,10 +188,22 @@ internal class CoordinatorIntegrationTest : TransportTest() { val rInputStream2 = ByteArrayInputStream(bOutputStream2.toByteArray()) every { kvRestorePlugin.listRecords(token, packageInfo) } returns listOf(key64, key264) every { outputFactory.getBackupDataOutput(fileDescriptor) } returns backupDataOutput - every { kvRestorePlugin.getInputStreamForRecord(token, packageInfo, key64) } returns rInputStream + coEvery { + kvRestorePlugin.getInputStreamForRecord( + token, + packageInfo, + key64 + ) + } returns rInputStream every { backupDataOutput.writeEntityHeader(key, appData.size) } returns 1137 every { backupDataOutput.writeEntityData(appData, appData.size) } returns appData.size - every { kvRestorePlugin.getInputStreamForRecord(token, packageInfo, key264) } returns rInputStream2 + coEvery { + kvRestorePlugin.getInputStreamForRecord( + token, + packageInfo, + key264 + ) + } returns rInputStream2 every { backupDataOutput.writeEntityHeader(key2, appData2.size) } returns 1137 every { backupDataOutput.writeEntityData(appData2, appData2.size) } returns appData2.size @@ -163,7 +218,7 @@ internal class CoordinatorIntegrationTest : TransportTest() { val bOutputStream = ByteArrayOutputStream() // read one key/value record and write it to output stream - every { kvBackupPlugin.hasDataForPackage(packageInfo) } returns false + coEvery { kvBackupPlugin.hasDataForPackage(packageInfo) } returns false coEvery { kvBackupPlugin.ensureRecordStorageForPackage(packageInfo) } just Runs every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput every { backupDataInput.readNextHeader() } returns true andThen false @@ -173,7 +228,12 @@ internal class CoordinatorIntegrationTest : TransportTest() { appData.copyInto(value.captured) // write the app data into the passed ByteArray appData.size } - coEvery { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key64) } returns bOutputStream + coEvery { + kvBackupPlugin.getOutputStreamForRecord( + packageInfo, + key64 + ) + } returns bOutputStream coEvery { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns null coEvery { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs @@ -197,7 +257,13 @@ internal class CoordinatorIntegrationTest : TransportTest() { val rInputStream = ByteArrayInputStream(bOutputStream.toByteArray()) every { kvRestorePlugin.listRecords(token, packageInfo) } returns listOf(key64) every { outputFactory.getBackupDataOutput(fileDescriptor) } returns backupDataOutput - every { kvRestorePlugin.getInputStreamForRecord(token, packageInfo, key64) } returns rInputStream + coEvery { + kvRestorePlugin.getInputStreamForRecord( + token, + packageInfo, + key64 + ) + } returns rInputStream every { backupDataOutput.writeEntityHeader(key, appData.size) } returns 1137 every { backupDataOutput.writeEntityData(appData, appData.size) } returns appData.size @@ -212,9 +278,21 @@ internal class CoordinatorIntegrationTest : TransportTest() { coEvery { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP - coEvery { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns packageMetadata + coEvery { + apkBackup.backupApkIfNecessary( + packageInfo, + UNKNOWN_ERROR, + any() + ) + } returns packageMetadata coEvery { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream - every { metadataManager.onApkBackedUp(packageInfo, packageMetadata, metadataOutputStream) } just Runs + every { + metadataManager.onApkBackedUp( + packageInfo, + packageMetadata, + metadataOutputStream + ) + } just Runs every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs // perform backup to output stream @@ -237,7 +315,12 @@ internal class CoordinatorIntegrationTest : TransportTest() { // reverse the backup streams into restore input val rInputStream = ByteArrayInputStream(bOutputStream.toByteArray()) val rOutputStream = ByteArrayOutputStream() - coEvery { fullRestorePlugin.getInputStreamForPackage(token, packageInfo) } returns rInputStream + coEvery { + fullRestorePlugin.getInputStreamForPackage( + token, + packageInfo + ) + } returns rInputStream every { outputFactory.getOutputStream(fileDescriptor) } returns rOutputStream // restore data 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 08566626..97c484ca 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 @@ -44,7 +44,18 @@ internal class BackupCoordinatorTest : BackupTest() { private val packageService: PackageService = mockk() private val notificationManager = mockk() - private val backup = BackupCoordinator(context, plugin, kv, full, apkBackup, clock, packageService, metadataManager, settingsManager, notificationManager) + private val backup = BackupCoordinator( + context, + plugin, + kv, + full, + apkBackup, + clock, + packageService, + metadataManager, + settingsManager, + notificationManager + ) private val metadataOutputStream = mockk() private val fileDescriptor: ParcelFileDescriptor = mockk() @@ -93,26 +104,27 @@ internal class BackupCoordinatorTest : BackupTest() { } @Test - fun `no error notification when device initialization fails on unplugged USB storage`() = runBlocking { - val storage = mockk() - val documentFile = mockk() + fun `no error notification when device initialization fails on unplugged USB storage`() = + runBlocking { + val storage = mockk() + val documentFile = mockk() - every { clock.time() } returns token - coEvery { plugin.initializeDevice(token) } throws IOException() - every { settingsManager.getStorage() } returns storage - every { storage.isUsb } returns true - every { storage.getDocumentFile(context) } returns documentFile - every { documentFile.isDirectory } returns false + every { clock.time() } returns token + coEvery { plugin.initializeDevice(token) } throws IOException() + every { settingsManager.getStorage() } returns storage + every { storage.isUsb } returns true + every { storage.getDocumentFile(context) } returns documentFile + every { documentFile.isDirectory } returns false - assertEquals(TRANSPORT_ERROR, backup.initializeDevice()) + assertEquals(TRANSPORT_ERROR, backup.initializeDevice()) - // finish will only be called when TRANSPORT_OK is returned, so it should throw - every { kv.hasState() } returns false - every { full.hasState() } returns false - coAssertThrows(IllegalStateException::class.java) { - backup.finishBackup() + // finish will only be called when TRANSPORT_OK is returned, so it should throw + every { kv.hasState() } returns false + every { full.hasState() } returns false + coAssertThrows(IllegalStateException::class.java) { + backup.finishBackup() + } } - } @Test fun `getBackupQuota() delegates to right plugin`() = runBlocking { @@ -143,24 +155,24 @@ internal class BackupCoordinatorTest : BackupTest() { } @Test - fun `clearing KV backup data throws`() { - every { kv.clearBackupData(packageInfo) } throws IOException() + fun `clearing KV backup data throws`() = runBlocking { + coEvery { kv.clearBackupData(packageInfo) } throws IOException() assertEquals(TRANSPORT_ERROR, backup.clearBackupData(packageInfo)) } @Test - fun `clearing full backup data throws`() { - every { kv.clearBackupData(packageInfo) } just Runs - every { full.clearBackupData(packageInfo) } throws IOException() + fun `clearing full backup data throws`() = runBlocking { + coEvery { kv.clearBackupData(packageInfo) } just Runs + coEvery { full.clearBackupData(packageInfo) } throws IOException() assertEquals(TRANSPORT_ERROR, backup.clearBackupData(packageInfo)) } @Test fun `clearing backup data succeeds`() = runBlocking { - every { kv.clearBackupData(packageInfo) } just Runs - every { full.clearBackupData(packageInfo) } just Runs + coEvery { kv.clearBackupData(packageInfo) } just Runs + coEvery { full.clearBackupData(packageInfo) } just Runs assertEquals(TRANSPORT_OK, backup.clearBackupData(packageInfo)) @@ -213,16 +225,28 @@ internal class BackupCoordinatorTest : BackupTest() { every { full.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP every { full.checkFullBackupSize(DEFAULT_QUOTA_FULL_BACKUP + 1) } returns TRANSPORT_QUOTA_EXCEEDED every { full.getCurrentPackage() } returns packageInfo - every { metadataManager.onPackageBackupError(packageInfo, QUOTA_EXCEEDED, metadataOutputStream) } just Runs - every { full.cancelFullBackup() } just Runs + every { + metadataManager.onPackageBackupError( + packageInfo, + QUOTA_EXCEEDED, + metadataOutputStream + ) + } just Runs + coEvery { full.cancelFullBackup() } just Runs every { settingsManager.getStorage() } returns storage - assertEquals(TRANSPORT_OK, - backup.performFullBackup(packageInfo, fileDescriptor, 0)) - assertEquals(DEFAULT_QUOTA_FULL_BACKUP, - backup.getBackupQuota(packageInfo.packageName, true)) - assertEquals(TRANSPORT_QUOTA_EXCEEDED, - backup.checkFullBackupSize(DEFAULT_QUOTA_FULL_BACKUP + 1)) + assertEquals( + TRANSPORT_OK, + backup.performFullBackup(packageInfo, fileDescriptor, 0) + ) + assertEquals( + DEFAULT_QUOTA_FULL_BACKUP, + backup.getBackupQuota(packageInfo.packageName, true) + ) + assertEquals( + TRANSPORT_QUOTA_EXCEEDED, + backup.checkFullBackupSize(DEFAULT_QUOTA_FULL_BACKUP + 1) + ) backup.cancelFullBackup() assertEquals(0L, backup.requestFullBackupTime()) @@ -238,14 +262,24 @@ internal class BackupCoordinatorTest : BackupTest() { every { full.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP every { full.checkFullBackupSize(0) } returns TRANSPORT_PACKAGE_REJECTED every { full.getCurrentPackage() } returns packageInfo - every { metadataManager.onPackageBackupError(packageInfo, NO_DATA, metadataOutputStream) } just Runs - every { full.cancelFullBackup() } just Runs + every { + metadataManager.onPackageBackupError( + packageInfo, + NO_DATA, + metadataOutputStream + ) + } just Runs + coEvery { full.cancelFullBackup() } just Runs every { settingsManager.getStorage() } returns storage - assertEquals(TRANSPORT_OK, - backup.performFullBackup(packageInfo, fileDescriptor, 0)) - assertEquals(DEFAULT_QUOTA_FULL_BACKUP, - backup.getBackupQuota(packageInfo.packageName, true)) + assertEquals( + TRANSPORT_OK, + backup.performFullBackup(packageInfo, fileDescriptor, 0) + ) + assertEquals( + DEFAULT_QUOTA_FULL_BACKUP, + backup.getBackupQuota(packageInfo.packageName, true) + ) assertEquals(TRANSPORT_PACKAGE_REJECTED, backup.checkFullBackupSize(0)) backup.cancelFullBackup() assertEquals(0L, backup.requestFullBackupTime()) @@ -259,24 +293,44 @@ internal class BackupCoordinatorTest : BackupTest() { fun `not allowed apps get their APKs backed up during @pm@ backup`() = runBlocking { val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER } val notAllowedPackages = listOf( - PackageInfo().apply { packageName = "org.example.1" }, - PackageInfo().apply { packageName = "org.example.2" } + PackageInfo().apply { packageName = "org.example.1" }, + PackageInfo().apply { packageName = "org.example.2" } ) val packageMetadata: PackageMetadata = mockk() - every { settingsManager.getStorage() } returns storage // to check for removable storage + every { settingsManager.getStorage() } returns storage // to check for removable storage every { packageService.notAllowedPackages } returns notAllowedPackages // no backup needed - coEvery { apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any()) } returns null + coEvery { + apkBackup.backupApkIfNecessary( + notAllowedPackages[0], + NOT_ALLOWED, + any() + ) + } returns null // was backed up, get new packageMetadata - coEvery { apkBackup.backupApkIfNecessary(notAllowedPackages[1], NOT_ALLOWED, any()) } returns packageMetadata + coEvery { + apkBackup.backupApkIfNecessary( + notAllowedPackages[1], + NOT_ALLOWED, + any() + ) + } returns packageMetadata coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream - every { metadataManager.onApkBackedUp(notAllowedPackages[1], packageMetadata, metadataOutputStream) } just Runs + every { + metadataManager.onApkBackedUp( + notAllowedPackages[1], + packageMetadata, + metadataOutputStream + ) + } just Runs // do actual @pm@ backup coEvery { kv.performBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK - assertEquals(TRANSPORT_OK, - backup.performIncrementalBackup(packageInfo, fileDescriptor, 0)) + assertEquals( + TRANSPORT_OK, + backup.performIncrementalBackup(packageInfo, fileDescriptor, 0) + ) coVerify { apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any()) @@ -285,9 +339,21 @@ internal class BackupCoordinatorTest : BackupTest() { } private fun expectApkBackupAndMetadataWrite() { - coEvery { apkBackup.backupApkIfNecessary(any(), UNKNOWN_ERROR, any()) } returns packageMetadata + coEvery { + apkBackup.backupApkIfNecessary( + any(), + UNKNOWN_ERROR, + any() + ) + } returns packageMetadata coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream - every { metadataManager.onApkBackedUp(any(), packageMetadata, metadataOutputStream) } just Runs + every { + metadataManager.onApkBackedUp( + any(), + packageMetadata, + metadataOutputStream + ) + } just Runs } } diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt index 58546342..4605ec2b 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt @@ -199,8 +199,8 @@ internal class FullBackupTest : BackupTest() { } @Test - fun `clearBackupData delegates to plugin`() { - every { plugin.removeDataOfPackage(packageInfo) } just Runs + fun `clearBackupData delegates to plugin`() = runBlocking { + coEvery { plugin.removeDataOfPackage(packageInfo) } just Runs backup.clearBackupData(packageInfo) } @@ -210,7 +210,7 @@ internal class FullBackupTest : BackupTest() { every { inputFactory.getInputStream(data) } returns inputStream expectInitializeOutputStream() expectClearState() - every { plugin.removeDataOfPackage(packageInfo) } just Runs + coEvery { plugin.removeDataOfPackage(packageInfo) } just Runs assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, data)) assertTrue(backup.hasState()) @@ -223,7 +223,7 @@ internal class FullBackupTest : BackupTest() { every { inputFactory.getInputStream(data) } returns inputStream expectInitializeOutputStream() expectClearState() - every { plugin.removeDataOfPackage(packageInfo) } throws IOException() + coEvery { plugin.removeDataOfPackage(packageInfo) } throws IOException() assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, data)) assertTrue(backup.hasState()) diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt index 31a3de81..2647e51e 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt @@ -54,7 +54,7 @@ internal class KVBackupTest : BackupTest() { @Test fun `incremental backup with no data gets rejected`() = runBlocking { - every { plugin.hasDataForPackage(packageInfo) } returns false + coEvery { plugin.hasDataForPackage(packageInfo) } returns false assertEquals(TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED, backup.performBackup(packageInfo, data, FLAG_INCREMENTAL)) assertFalse(backup.hasState()) @@ -62,7 +62,7 @@ internal class KVBackupTest : BackupTest() { @Test fun `check for existing data throws exception`() = runBlocking { - every { plugin.hasDataForPackage(packageInfo) } throws IOException() + coEvery { plugin.hasDataForPackage(packageInfo) } throws IOException() assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0)) assertFalse(backup.hasState()) @@ -71,7 +71,7 @@ internal class KVBackupTest : BackupTest() { @Test fun `non-incremental backup with data clears old data first`() = runBlocking { singleRecordBackup(true) - every { plugin.removeDataOfPackage(packageInfo) } just Runs + coEvery { plugin.removeDataOfPackage(packageInfo) } just Runs assertEquals(TRANSPORT_OK, backup.performBackup(packageInfo, data, FLAG_NON_INCREMENTAL)) assertTrue(backup.hasState()) @@ -82,7 +82,7 @@ internal class KVBackupTest : BackupTest() { @Test fun `ignoring exception when clearing data when non-incremental backup has data`() = runBlocking { singleRecordBackup(true) - every { plugin.removeDataOfPackage(packageInfo) } throws IOException() + coEvery { plugin.removeDataOfPackage(packageInfo) } throws IOException() assertEquals(TRANSPORT_OK, backup.performBackup(packageInfo, data, FLAG_NON_INCREMENTAL)) assertTrue(backup.hasState()) @@ -92,7 +92,7 @@ internal class KVBackupTest : BackupTest() { @Test fun `ensuring storage throws exception`() = runBlocking { - every { plugin.hasDataForPackage(packageInfo) } returns false + coEvery { plugin.hasDataForPackage(packageInfo) } returns false coEvery { plugin.ensureRecordStorageForPackage(packageInfo) } throws IOException() assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0)) @@ -194,7 +194,7 @@ internal class KVBackupTest : BackupTest() { } private fun initPlugin(hasDataForPackage: Boolean = false) { - every { plugin.hasDataForPackage(packageInfo) } returns hasDataForPackage + coEvery { plugin.hasDataForPackage(packageInfo) } returns hasDataForPackage coEvery { plugin.ensureRecordStorageForPackage(packageInfo) } just Runs } diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt index e9692cf8..e63c69da 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt @@ -3,6 +3,7 @@ package com.stevesoltys.seedvault.transport.restore import android.app.backup.BackupDataOutput import android.app.backup.BackupTransport.TRANSPORT_ERROR import android.app.backup.BackupTransport.TRANSPORT_OK +import com.stevesoltys.seedvault.coAssertThrows import com.stevesoltys.seedvault.encodeBase64 import com.stevesoltys.seedvault.getRandomByteArray import com.stevesoltys.seedvault.header.UnsupportedVersionException @@ -16,7 +17,6 @@ import io.mockk.mockk import io.mockk.verifyAll import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import java.io.IOException import java.io.InputStream @@ -47,13 +47,13 @@ internal class KVRestoreTest : RestoreTest() { @Test fun `getRestoreData() throws without initializing state`() { - assertThrows(IllegalStateException::class.java) { + coAssertThrows(IllegalStateException::class.java) { restore.getRestoreData(fileDescriptor) } } @Test - fun `listing records throws`() { + fun `listing records throws`() = runBlocking { restore.initializeState(token, packageInfo) every { plugin.listRecords(token, packageInfo) } throws IOException() @@ -62,11 +62,11 @@ internal class KVRestoreTest : RestoreTest() { } @Test - fun `reading VersionHeader with unsupported version throws`() { + fun `reading VersionHeader with unsupported version throws`() = runBlocking { restore.initializeState(token, packageInfo) getRecordsAndOutput() - every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream + coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream every { headerReader.readVersion(inputStream) } throws UnsupportedVersionException(unsupportedVersion) streamsGetClosed() @@ -75,11 +75,11 @@ internal class KVRestoreTest : RestoreTest() { } @Test - fun `error reading VersionHeader throws`() { + fun `error reading VersionHeader throws`() = runBlocking { restore.initializeState(token, packageInfo) getRecordsAndOutput() - every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream + coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream every { headerReader.readVersion(inputStream) } throws IOException() streamsGetClosed() @@ -88,11 +88,11 @@ internal class KVRestoreTest : RestoreTest() { } @Test - fun `decrypting segment throws`() { + fun `decrypting segment throws`() = runBlocking { restore.initializeState(token, packageInfo) getRecordsAndOutput() - every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream + coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream every { headerReader.readVersion(inputStream) } returns VERSION every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader every { crypto.decryptMultipleSegments(inputStream) } throws IOException() @@ -103,11 +103,11 @@ internal class KVRestoreTest : RestoreTest() { } @Test - fun `decrypting header throws`() { + fun `decrypting header throws`() = runBlocking { restore.initializeState(token, packageInfo) getRecordsAndOutput() - every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream + coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream every { headerReader.readVersion(inputStream) } returns VERSION every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } throws IOException() streamsGetClosed() @@ -117,11 +117,11 @@ internal class KVRestoreTest : RestoreTest() { } @Test - fun `decrypting header throws security exception`() { + fun `decrypting header throws security exception`() = runBlocking { restore.initializeState(token, packageInfo) getRecordsAndOutput() - every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream + coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream every { headerReader.readVersion(inputStream) } returns VERSION every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } throws SecurityException() streamsGetClosed() @@ -131,11 +131,11 @@ internal class KVRestoreTest : RestoreTest() { } @Test - fun `writing header throws`() { + fun `writing header throws`() = runBlocking { restore.initializeState(token, packageInfo) getRecordsAndOutput() - every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream + coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream every { headerReader.readVersion(inputStream) } returns VERSION every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader every { crypto.decryptMultipleSegments(inputStream) } returns data @@ -147,11 +147,11 @@ internal class KVRestoreTest : RestoreTest() { } @Test - fun `writing value throws`() { + fun `writing value throws`() = runBlocking { restore.initializeState(token, packageInfo) getRecordsAndOutput() - every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream + coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream every { headerReader.readVersion(inputStream) } returns VERSION every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader every { crypto.decryptMultipleSegments(inputStream) } returns data @@ -164,11 +164,11 @@ internal class KVRestoreTest : RestoreTest() { } @Test - fun `writing value succeeds`() { + fun `writing value succeeds`() = runBlocking { restore.initializeState(token, packageInfo) getRecordsAndOutput() - every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream + coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream every { headerReader.readVersion(inputStream) } returns VERSION every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader every { crypto.decryptMultipleSegments(inputStream) } returns data @@ -181,21 +181,21 @@ internal class KVRestoreTest : RestoreTest() { } @Test - fun `writing two values succeeds`() { + fun `writing two values succeeds`() = runBlocking { val data2 = getRandomByteArray() val inputStream2 = mockk() restore.initializeState(token, packageInfo) getRecordsAndOutput(listOf(key64, key264)) // first key/value - every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream + coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream every { headerReader.readVersion(inputStream) } returns VERSION every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader every { crypto.decryptMultipleSegments(inputStream) } returns data every { output.writeEntityHeader(key, data.size) } returns 42 every { output.writeEntityData(data, data.size) } returns data.size // second key/value - every { plugin.getInputStreamForRecord(token, packageInfo, key264) } returns inputStream2 + coEvery { plugin.getInputStreamForRecord(token, packageInfo, key264) } returns inputStream2 every { headerReader.readVersion(inputStream2) } returns VERSION every { crypto.decryptHeader(inputStream2, VERSION, packageInfo.packageName, key2) } returns versionHeader2 every { crypto.decryptMultipleSegments(inputStream2) } returns data2 diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt index 05cf90b3..7ce78cf5 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt @@ -44,7 +44,16 @@ internal class RestoreCoordinatorTest : TransportTest() { private val full = mockk() private val metadataReader = mockk() - private val restore = RestoreCoordinator(context, settingsManager, metadataManager, notificationManager, plugin, kv, full, metadataReader) + private val restore = RestoreCoordinator( + context, + settingsManager, + metadataManager, + notificationManager, + plugin, + kv, + full, + metadataReader + ) private val token = Random.nextLong() private val inputStream = mockk() @@ -219,11 +228,11 @@ internal class RestoreCoordinatorTest : TransportTest() { } @Test - fun `getRestoreData() delegates to KV`() { + fun `getRestoreData() delegates to KV`() = runBlocking { val data = mockk() val result = Random.nextInt() - every { kv.getRestoreData(data) } returns result + coEvery { kv.getRestoreData(data) } returns result assertEquals(result, restore.getRestoreData(data)) }