diff --git a/.gitignore b/.gitignore
index 8e59b12e..9e827658 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,7 +7,8 @@ hs_err_pid*
 ## Intellij
 out/
 lib/
-.idea/
+.idea/*
+!.idea/runConfigurations*
 *.ipr
 *.iws
 *.iml
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 00000000..7f68460d
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="RunConfigurationProducerService">
+    <option name="ignoredProducers">
+      <set>
+        <option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
+        <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
+        <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
+      </set>
+    </option>
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/runConfigurations/Unit_Tests.xml b/.idea/runConfigurations/Unit_Tests.xml
new file mode 100644
index 00000000..14960c4b
--- /dev/null
+++ b/.idea/runConfigurations/Unit_Tests.xml
@@ -0,0 +1,17 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="Unit Tests" type="AndroidJUnit" factoryName="Android JUnit">
+    <module name="app" />
+    <useClassPathOnly />
+    <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
+    <option name="ALTERNATIVE_JRE_PATH" value="/usr/lib/jvm/java-11" />
+    <option name="MAIN_CLASS_NAME" value="" />
+    <option name="METHOD_NAME" value="" />
+    <option name="TEST_OBJECT" value="directory" />
+    <option name="PARAMETERS" value="" />
+    <option name="WORKING_DIRECTORY" value="$MODULE_DIR$" />
+    <dir value="$PROJECT_DIR$/app/src/test/java/com/stevesoltys/seedvault" />
+    <method v="2">
+      <option name="Android.Gradle.BeforeRunTask" enabled="true" />
+    </method>
+  </configuration>
+</component>
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index afcf4356..685e0dd3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -7,7 +7,7 @@ apply plugin: 'kotlin-android-extensions'
 android {
 
     compileSdkVersion 29
-    buildToolsVersion '29.0.2'
+    buildToolsVersion '29.0.2'  // adapt in .travis.yaml if changed here
 
     defaultConfig {
         minSdkVersion 29
@@ -128,9 +128,9 @@ dependencies {
 
     lintChecks 'com.github.thirdegg:lint-rules:0.0.4-alpha'
 
-    def junit_version = "5.5.2"
+    def junit_version = "5.5.2"  // careful, upgrading this can change a Cipher's IV size in tests!?
     def mockk_version = "1.10.0"
-    testImplementation aospDeps
+    testImplementation aospDeps  // anything less fails tests run with gradlew
     testImplementation 'androidx.test.ext:junit:1.1.1'
     testImplementation 'org.robolectric:robolectric:4.3.1'
     testImplementation "org.junit.jupiter:junit-jupiter-api:$junit_version"
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/DocumentsStorageTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/DocumentsStorageTest.kt
deleted file mode 100644
index 6a9cba82..00000000
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/DocumentsStorageTest.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-package com.stevesoltys.seedvault
-
-import androidx.documentfile.provider.DocumentFile
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.stevesoltys.seedvault.metadata.MetadataManager
-import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
-import com.stevesoltys.seedvault.plugins.saf.createOrGetFile
-import com.stevesoltys.seedvault.settings.SettingsManager
-import org.junit.After
-import org.junit.Assert.assertArrayEquals
-import org.junit.Assert.assertNotNull
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.koin.core.KoinComponent
-import org.koin.core.inject
-import kotlin.random.Random
-
-private const val filename = "test-file"
-
-@RunWith(AndroidJUnit4::class)
-class DocumentsStorageTest : KoinComponent {
-
-    private val context = InstrumentationRegistry.getInstrumentation().targetContext
-    private val metadataManager by inject<MetadataManager>()
-    private val settingsManager by inject<SettingsManager>()
-    private val storage = DocumentsStorage(context, metadataManager, settingsManager)
-
-    private lateinit var file: DocumentFile
-
-    @Before
-    fun setup() {
-        assertNotNull("Select a storage location in the app first!", storage.rootBackupDir)
-        file = storage.rootBackupDir?.createOrGetFile(filename)
-                ?: throw RuntimeException("Could not create test file")
-    }
-
-    @After
-    fun tearDown() {
-        file.delete()
-    }
-
-    @Test
-    fun testWritingAndReadingFile() {
-        // write to output stream
-        val outputStream = storage.getOutputStream(file)
-        val content = ByteArray(1337).apply { Random.nextBytes(this) }
-        outputStream.write(content)
-        outputStream.flush()
-        outputStream.close()
-
-        // read written data from input stream
-        val inputStream = storage.getInputStream(file)
-        val readContent = inputStream.readBytes()
-        inputStream.close()
-        assertArrayEquals(content, readContent)
-
-        // write smaller content to same file
-        val outputStream2 = storage.getOutputStream(file)
-        val content2 = ByteArray(42).apply { Random.nextBytes(this) }
-        outputStream2.write(content2)
-        outputStream2.flush()
-        outputStream2.close()
-
-        // read written data from input stream
-        val inputStream2 = storage.getInputStream(file)
-        val readContent2 = inputStream2.readBytes()
-        inputStream2.close()
-        assertArrayEquals(content2, readContent2)
-    }
-
-}
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
index d2251636..ed464b03 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
@@ -13,11 +13,11 @@ import com.stevesoltys.seedvault.transport.backup.BackupPlugin
 import com.stevesoltys.seedvault.transport.restore.RestorePlugin
 import io.mockk.every
 import io.mockk.mockk
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
 import org.junit.After
-import org.junit.Assert.assertArrayEquals
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
-import org.junit.Assert.assertNotEquals
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertTrue
 import org.junit.Before
@@ -25,12 +25,11 @@ import org.junit.Test
 import org.junit.runner.RunWith
 import org.koin.core.KoinComponent
 import org.koin.core.inject
-import java.io.InputStream
-import java.io.OutputStream
 import kotlin.random.Random
 
 
 @RunWith(AndroidJUnit4::class)
+@Suppress("BlockingMethodInNonBlockingContext")
 class PluginTest : KoinComponent {
 
     private val context = InstrumentationRegistry.getInstrumentation().targetContext
@@ -38,7 +37,7 @@ class PluginTest : KoinComponent {
     private val settingsManager: SettingsManager by inject()
     private val mockedSettingsManager: SettingsManager = mockk()
     private val storage = DocumentsStorage(context, metadataManager, mockedSettingsManager)
-    private val backupPlugin: BackupPlugin = DocumentsProviderBackupPlugin(storage, context.packageManager)
+    private val backupPlugin: BackupPlugin = DocumentsProviderBackupPlugin(context, storage)
     private val restorePlugin: RestorePlugin = DocumentsProviderRestorePlugin(context, storage)
 
     private val token = Random.nextLong()
@@ -70,7 +69,7 @@ class PluginTest : KoinComponent {
      * that needs to get re-queried to get real results.
      */
     @Test
-    fun testInitializationAndRestoreSets() {
+    fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) {
         // no backups available initially
         assertEquals(0, restorePlugin.getAvailableBackups()?.toList()?.size)
         val uri = settingsManager.getStorage()?.getDocumentFile(context)?.uri ?: error("no storage")
@@ -104,7 +103,7 @@ class PluginTest : KoinComponent {
     }
 
     @Test
-    fun testMetadataWriteRead() {
+    fun testMetadataWriteRead() = runBlocking(Dispatchers.IO) {
         every { mockedSettingsManager.getAndResetIsStorageChanging() } returns true andThen false
         assertTrue(backupPlugin.initializeDevice(newToken = token))
 
@@ -120,7 +119,7 @@ class PluginTest : KoinComponent {
         assertFalse(availableBackups[0].error)
 
         // read metadata matches what was written earlier
-        assertEquals(metadata, availableBackups[0].inputStream)
+        assertReadEquals(metadata, availableBackups[0].inputStream)
 
         // initializing again (without changing storage) keeps restore set with same token
         assertFalse(backupPlugin.initializeDevice(newToken = token + 1))
@@ -131,11 +130,11 @@ class PluginTest : KoinComponent {
         assertFalse(availableBackups[0].error)
 
         // metadata hasn't changed
-        assertEquals(metadata, availableBackups[0].inputStream)
+        assertReadEquals(metadata, availableBackups[0].inputStream)
     }
 
     @Test
-    fun testApkWriteRead() {
+    fun testApkWriteRead() = runBlocking {
         // initialize storage with given token
         initStorage(token)
 
@@ -144,11 +143,11 @@ class PluginTest : KoinComponent {
         backupPlugin.getApkOutputStream(packageInfo).writeAndClose(apk)
 
         // assert that read APK bytes match what was written
-        assertEquals(apk, restorePlugin.getApkInputStream(token, packageInfo.packageName))
+        assertReadEquals(apk, restorePlugin.getApkInputStream(token, packageInfo.packageName))
     }
 
     @Test
-    fun testKvBackupRestore() {
+    fun testKvBackupRestore() = runBlocking {
         // define shortcuts
         val kvBackup = backupPlugin.kvBackupPlugin
         val kvRestore = restorePlugin.kvRestorePlugin
@@ -178,7 +177,7 @@ class PluginTest : KoinComponent {
         var records = kvRestore.listRecords(token, packageInfo)
         assertEquals(1, records.size)
         assertEquals(record1.first, records[0])
-        assertEquals(record1.second, kvRestore.getInputStreamForRecord(token, packageInfo, record1.first))
+        assertReadEquals(record1.second, kvRestore.getInputStreamForRecord(token, packageInfo, record1.first))
 
         // write second and third record
         kvBackup.ensureRecordStorageForPackage(packageInfo)
@@ -188,9 +187,9 @@ class PluginTest : KoinComponent {
         // all records for package are found and returned properly
         records = kvRestore.listRecords(token, packageInfo)
         assertEquals(listOf(record1.first, record2.first, record3.first).sorted(), records.sorted())
-        assertEquals(record1.second, kvRestore.getInputStreamForRecord(token, packageInfo, record1.first))
-        assertEquals(record2.second, kvRestore.getInputStreamForRecord(token, packageInfo, record2.first))
-        assertEquals(record3.second, kvRestore.getInputStreamForRecord(token, packageInfo, record3.first))
+        assertReadEquals(record1.second, kvRestore.getInputStreamForRecord(token, packageInfo, record1.first))
+        assertReadEquals(record2.second, kvRestore.getInputStreamForRecord(token, packageInfo, record2.first))
+        assertReadEquals(record3.second, kvRestore.getInputStreamForRecord(token, packageInfo, record3.first))
 
         // delete record3 and ensure that the other two are still found
         kvBackup.deleteRecord(packageInfo, record3.first)
@@ -204,7 +203,7 @@ class PluginTest : KoinComponent {
     }
 
     @Test
-    fun testMaxKvKeyLength() {
+    fun testMaxKvKeyLength() = runBlocking {
         // define shortcuts
         val kvBackup = backupPlugin.kvBackupPlugin
         val kvRestore = restorePlugin.kvRestorePlugin
@@ -222,21 +221,18 @@ class PluginTest : KoinComponent {
 
         // max record is found correctly
         assertTrue(kvRestore.hasDataForPackage(token, packageInfo))
-        var records = kvRestore.listRecords(token, packageInfo)
+        val records = kvRestore.listRecords(token, packageInfo)
         assertEquals(listOf(recordMax.first), records)
 
         // write exceeding key length record
         kvBackup.ensureRecordStorageForPackage(packageInfo)
-        kvBackup.getOutputStreamForRecord(packageInfo, recordOver.first).writeAndClose(recordOver.second)
-
-        // exceeding record gets truncated
-        assertTrue(kvRestore.hasDataForPackage(token, packageInfo))
-        records = kvRestore.listRecords(token, packageInfo)
-        assertNotEquals(listOf(recordMax.first, recordOver.first).sorted(), records.sorted())
+        coAssertThrows(IllegalStateException::class.java) {
+            kvBackup.getOutputStreamForRecord(packageInfo, recordOver.first).writeAndClose(recordOver.second)
+        }
     }
 
     @Test
-    fun testFullBackupRestore() {
+    fun testFullBackupRestore() = runBlocking {
         // define shortcuts
         val fullBackup = backupPlugin.fullBackupPlugin
         val fullRestore = restorePlugin.fullRestorePlugin
@@ -257,13 +253,13 @@ class PluginTest : KoinComponent {
         assertFalse(fullRestore.hasDataForPackage(token + 1, packageInfo))
 
         // restore data matches backed up data
-        assertEquals(data, fullRestore.getInputStreamForPackage(token, packageInfo))
+        assertReadEquals(data, fullRestore.getInputStreamForPackage(token, packageInfo))
 
         // write and check data for second package
         val data2 = getRandomByteArray(5 * 1024 * 1024)
         fullBackup.getOutputStream(packageInfo2).writeAndClose(data2)
         assertTrue(fullRestore.hasDataForPackage(token, packageInfo2))
-        assertEquals(data2, fullRestore.getInputStreamForPackage(token, packageInfo2))
+        assertReadEquals(data2, fullRestore.getInputStreamForPackage(token, packageInfo2))
 
         // remove data of first package again and ensure that no more data is found
         fullBackup.removeDataOfPackage(packageInfo)
@@ -277,17 +273,9 @@ class PluginTest : KoinComponent {
         assertFalse(fullRestore.hasDataForPackage(token, packageInfo2))
     }
 
-    private fun initStorage(token: Long) {
+    private fun initStorage(token: Long) = runBlocking {
         every { mockedSettingsManager.getAndResetIsStorageChanging() } returns true
         assertTrue(backupPlugin.initializeDevice(newToken = token))
     }
 
-    private fun OutputStream.writeAndClose(data: ByteArray) = use {
-        it.write(data)
-    }
-
-    private fun assertEquals(data: ByteArray, inputStream: InputStream?) = inputStream?.use {
-        assertArrayEquals(data, it.readBytes())
-    } ?: error("no input stream")
-
 }
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt
new file mode 100644
index 00000000..8246820f
--- /dev/null
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt
@@ -0,0 +1,174 @@
+package com.stevesoltys.seedvault.plugins.saf
+
+import android.database.ContentObserver
+import android.database.Cursor
+import android.net.Uri
+import android.os.Bundle
+import android.provider.DocumentsContract.EXTRA_LOADING
+import androidx.documentfile.provider.DocumentFile
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.stevesoltys.seedvault.assertReadEquals
+import com.stevesoltys.seedvault.coAssertThrows
+import com.stevesoltys.seedvault.getRandomBase64
+import com.stevesoltys.seedvault.getRandomByteArray
+import com.stevesoltys.seedvault.metadata.MetadataManager
+import com.stevesoltys.seedvault.settings.SettingsManager
+import com.stevesoltys.seedvault.writeAndClose
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.slot
+import io.mockk.verify
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.koin.core.KoinComponent
+import org.koin.core.inject
+import java.io.IOException
+import kotlin.random.Random
+
+@RunWith(AndroidJUnit4::class)
+@Suppress("BlockingMethodInNonBlockingContext")
+class DocumentsStorageTest : KoinComponent {
+
+    private val context = InstrumentationRegistry.getInstrumentation().targetContext
+    private val metadataManager by inject<MetadataManager>()
+    private val settingsManager by inject<SettingsManager>()
+    private val storage = DocumentsStorage(context, metadataManager, settingsManager)
+
+    private val filename = getRandomBase64()
+    private lateinit var file: DocumentFile
+
+    @Before
+    fun setup() = runBlocking {
+        assertNotNull("Select a storage location in the app first!", storage.rootBackupDir)
+        file = storage.rootBackupDir?.createOrGetFile(context, filename)
+                ?: throw RuntimeException("Could not create test file")
+    }
+
+    @After
+    fun tearDown() {
+        file.delete()
+    }
+
+    @Test
+    fun testWritingAndReadingFile() {
+        // write to output stream
+        val outputStream = storage.getOutputStream(file)
+        val content = ByteArray(1337).apply { Random.nextBytes(this) }
+        outputStream.write(content)
+        outputStream.flush()
+        outputStream.close()
+
+        // read written data from input stream
+        val inputStream = storage.getInputStream(file)
+        val readContent = inputStream.readBytes()
+        inputStream.close()
+        assertArrayEquals(content, readContent)
+
+        // write smaller content to same file
+        val outputStream2 = storage.getOutputStream(file)
+        val content2 = ByteArray(42).apply { Random.nextBytes(this) }
+        outputStream2.write(content2)
+        outputStream2.flush()
+        outputStream2.close()
+
+        // read written data from input stream
+        val inputStream2 = storage.getInputStream(file)
+        val readContent2 = inputStream2.readBytes()
+        inputStream2.close()
+        assertArrayEquals(content2, readContent2)
+    }
+
+    @Test
+    fun testFindFile() = runBlocking(Dispatchers.IO) {
+        val foundFile = storage.rootBackupDir!!.findFileBlocking(context, file.name!!)
+        assertNotNull(foundFile)
+        assertEquals(filename, foundFile!!.name)
+    }
+
+    @Test
+    fun testCreateFile() {
+        // create test file
+        val dir = storage.rootBackupDir!!
+        val createdFile = dir.createFile("text", getRandomBase64())
+        assertNotNull(createdFile)
+        assertNotNull(createdFile!!.name)
+
+        // write some data into it
+        val data = getRandomByteArray()
+        context.contentResolver.openOutputStream(createdFile.uri)!!.writeAndClose(data)
+
+        // data should still be there
+        assertReadEquals(data, context.contentResolver.openInputStream(createdFile.uri))
+
+        // delete again
+        createdFile.delete()
+        assertFalse(createdFile.exists())
+    }
+
+    @Test
+    fun testGetLoadedCursor() = runBlocking {
+        // empty cursor extras are like not loading, returns same cursor right away
+        val cursor1: Cursor = mockk()
+        every { cursor1.extras } returns Bundle()
+        assertEquals(cursor1, getLoadedCursor { cursor1 })
+
+        // explicitly not loading, returns same cursor right away
+        val cursor2: Cursor = mockk()
+        every { cursor2.extras } returns Bundle().apply { putBoolean(EXTRA_LOADING, false) }
+        assertEquals(cursor2, getLoadedCursor { cursor2 })
+
+        // loading cursor registers content observer, times out and closes cursor
+        val cursor3: Cursor = mockk()
+        every { cursor3.extras } returns Bundle().apply { putBoolean(EXTRA_LOADING, true) }
+        every { cursor3.registerContentObserver(any()) } just Runs
+        every { cursor3.close() } just Runs
+        coAssertThrows(TimeoutCancellationException::class.java) {
+            getLoadedCursor(1000) { cursor3 }
+        }
+        verify { cursor3.registerContentObserver(any()) }
+        verify { cursor3.close() } // ensure that cursor gets closed
+
+        // loading cursor registers content observer, but re-query fails
+        val cursor4: Cursor = mockk()
+        val observer4 = slot<ContentObserver>()
+        val query: () -> Cursor? = { if (observer4.isCaptured) null else cursor4 }
+        every { cursor4.extras } returns Bundle().apply { putBoolean(EXTRA_LOADING, true) }
+        every { cursor4.registerContentObserver(capture(observer4)) } answers {
+            observer4.captured.onChange(false, Uri.parse("foo://bar"))
+        }
+        every { cursor4.close() } just Runs
+        coAssertThrows(IOException::class.java) {
+            getLoadedCursor(10_000, query)
+        }
+        assertTrue(observer4.isCaptured)
+        verify { cursor4.close() } // ensure that cursor gets closed
+
+        // loading cursor registers content observer, re-queries and returns new result
+        val cursor5: Cursor = mockk()
+        val result5: Cursor = mockk()
+        val observer5 = slot<ContentObserver>()
+        val query5: () -> Cursor? = { if (observer5.isCaptured) result5 else cursor5 }
+        every { cursor5.extras } returns Bundle().apply { putBoolean(EXTRA_LOADING, true) }
+        every { cursor5.registerContentObserver(capture(observer5)) } answers {
+            observer5.captured.onChange(false, null)
+        }
+        every { cursor5.close() } just Runs
+        assertEquals(result5, getLoadedCursor(10_000, query5))
+        assertTrue(observer5.isCaptured)
+        verify { cursor5.close() } // ensure that initial cursor got closed
+    }
+
+}
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 89e4c74e..72f2a86a 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
@@ -1,5 +1,6 @@
 package com.stevesoltys.seedvault.plugins.saf
 
+import android.content.Context
 import android.content.pm.PackageInfo
 import android.content.pm.PackageManager
 import com.stevesoltys.seedvault.transport.backup.BackupPlugin
@@ -11,19 +12,21 @@ import java.io.OutputStream
 private const val MIME_TYPE_APK = "application/vnd.android.package-archive"
 
 internal class DocumentsProviderBackupPlugin(
-        private val storage: DocumentsStorage,
-        packageManager: PackageManager) : BackupPlugin {
+        private val context: Context,
+        private val storage: DocumentsStorage) : BackupPlugin {
+
+    private val packageManager: PackageManager = context.packageManager
 
     override val kvBackupPlugin: KVBackupPlugin by lazy {
-        DocumentsProviderKVBackup(storage)
+        DocumentsProviderKVBackup(storage, context)
     }
 
     override val fullBackupPlugin: FullBackupPlugin by lazy {
-        DocumentsProviderFullBackup(storage)
+        DocumentsProviderFullBackup(storage, context)
     }
 
     @Throws(IOException::class)
-    override fun initializeDevice(newToken: Long): Boolean {
+    override suspend fun initializeDevice(newToken: Long): Boolean {
         // check if storage is already initialized
         if (storage.isInitialized()) return false
 
@@ -46,16 +49,16 @@ internal class DocumentsProviderBackupPlugin(
     }
 
     @Throws(IOException::class)
-    override fun getMetadataOutputStream(): OutputStream {
+    override suspend fun getMetadataOutputStream(): OutputStream {
         val setDir = storage.getSetDir() ?: throw IOException()
-        val metadataFile = setDir.createOrGetFile(FILE_BACKUP_METADATA)
+        val metadataFile = setDir.createOrGetFile(context, FILE_BACKUP_METADATA)
         return storage.getOutputStream(metadataFile)
     }
 
     @Throws(IOException::class)
-    override fun getApkOutputStream(packageInfo: PackageInfo): OutputStream {
+    override suspend fun getApkOutputStream(packageInfo: PackageInfo): OutputStream {
         val setDir = storage.getSetDir() ?: throw IOException()
-        val file = setDir.createOrGetFile("${packageInfo.packageName}.apk", MIME_TYPE_APK)
+        val file = setDir.createOrGetFile(context, "${packageInfo.packageName}.apk", MIME_TYPE_APK)
         return storage.getOutputStream(file)
     }
 
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 0e6105ae..70e0499f 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
@@ -1,5 +1,6 @@
 package com.stevesoltys.seedvault.plugins.saf
 
+import android.content.Context
 import android.content.pm.PackageInfo
 import android.util.Log
 import com.stevesoltys.seedvault.transport.backup.DEFAULT_QUOTA_FULL_BACKUP
@@ -10,13 +11,14 @@ import java.io.OutputStream
 private val TAG = DocumentsProviderFullBackup::class.java.simpleName
 
 internal class DocumentsProviderFullBackup(
-        private val storage: DocumentsStorage) : FullBackupPlugin {
+        private val storage: DocumentsStorage,
+        private val context: Context) : FullBackupPlugin {
 
     override fun getQuota() = DEFAULT_QUOTA_FULL_BACKUP
 
     @Throws(IOException::class)
-    override fun getOutputStream(targetPackage: PackageInfo): OutputStream {
-        val file = storage.currentFullBackupDir?.createOrGetFile(targetPackage.packageName)
+    override suspend fun getOutputStream(targetPackage: PackageInfo): OutputStream {
+        val file = storage.currentFullBackupDir?.createOrGetFile(context, targetPackage.packageName)
                 ?: throw IOException()
         return storage.getOutputStream(file)
     }
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 57a0ae18..61b815be 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
@@ -9,13 +9,13 @@ internal class DocumentsProviderFullRestorePlugin(
         private val documentsStorage: DocumentsStorage) : FullRestorePlugin {
 
     @Throws(IOException::class)
-    override fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean {
+    override suspend fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean {
         val backupDir = documentsStorage.getFullBackupDir(token) ?: return false
         return backupDir.findFile(packageInfo.packageName) != null
     }
 
     @Throws(IOException::class)
-    override 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()
         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 b0bc45d9..7e4ad775 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
@@ -1,5 +1,6 @@
 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.backup.DEFAULT_QUOTA_KEY_VALUE_BACKUP
@@ -7,7 +8,10 @@ import com.stevesoltys.seedvault.transport.backup.KVBackupPlugin
 import java.io.IOException
 import java.io.OutputStream
 
-internal class DocumentsProviderKVBackup(private val storage: DocumentsStorage) : KVBackupPlugin {
+internal class DocumentsProviderKVBackup(
+        private val storage: DocumentsStorage,
+        private val context: Context
+) : KVBackupPlugin {
 
     private var packageFile: DocumentFile? = null
 
@@ -21,9 +25,9 @@ internal class DocumentsProviderKVBackup(private val storage: DocumentsStorage)
     }
 
     @Throws(IOException::class)
-    override fun ensureRecordStorageForPackage(packageInfo: PackageInfo) {
+    override suspend fun ensureRecordStorageForPackage(packageInfo: PackageInfo) {
         // remember package file for subsequent operations
-        packageFile = storage.getOrCreateKVBackupDir().createOrGetDirectory(packageInfo.packageName)
+        packageFile = storage.getOrCreateKVBackupDir().createOrGetDirectory(context, packageInfo.packageName)
     }
 
     @Throws(IOException::class)
@@ -43,10 +47,10 @@ internal class DocumentsProviderKVBackup(private val storage: DocumentsStorage)
     }
 
     @Throws(IOException::class)
-    override 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(key)
+        val keyFile = packageFile.createOrGetFile(context, key)
         return storage.getOutputStream(keyFile)
     }
 
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 7cb54f17..47253160 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
@@ -10,7 +10,7 @@ internal class DocumentsProviderKVRestorePlugin(private val storage: DocumentsSt
 
     private var packageDir: DocumentFile? = null
 
-    override fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean {
+    override suspend fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean {
         return try {
             val backupDir = storage.getKVBackupDir(token) ?: return false
             // remember package file for subsequent operations
diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderModule.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderModule.kt
index 1b0f84b7..66b22b7f 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderModule.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderModule.kt
@@ -7,6 +7,6 @@ import org.koin.dsl.module
 
 val documentsProviderModule = module {
     single { DocumentsStorage(androidContext(), get(), get()) }
-    single<BackupPlugin> { DocumentsProviderBackupPlugin(get(), androidContext().packageManager) }
+    single<BackupPlugin> { DocumentsProviderBackupPlugin(androidContext(), get()) }
     single<RestorePlugin> { DocumentsProviderRestorePlugin(androidContext(), get()) }
 }
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 142046ab..98050d2d 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
@@ -15,6 +15,8 @@ import java.io.InputStream
 
 private val TAG = DocumentsProviderRestorePlugin::class.java.simpleName
 
+@WorkerThread
+@Suppress("BlockingMethodInNonBlockingContext")  // all methods do I/O
 internal class DocumentsProviderRestorePlugin(
         private val context: Context,
         private val storage: DocumentsStorage) : RestorePlugin {
@@ -27,15 +29,15 @@ internal class DocumentsProviderRestorePlugin(
         DocumentsProviderFullRestorePlugin(storage)
     }
 
-    @WorkerThread
-    override fun hasBackup(uri: Uri): Boolean {
+    @Throws(IOException::class)
+    override suspend fun hasBackup(uri: Uri): Boolean {
         val parent = DocumentFile.fromTreeUri(context, uri) ?: throw AssertionError()
         val rootDir = parent.findFileBlocking(context, DIRECTORY_ROOT) ?: return false
         val backupSets = getBackups(context, rootDir)
         return backupSets.isNotEmpty()
     }
 
-    override fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>? {
+    override suspend fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>? {
         val rootDir = storage.rootBackupDir ?: return null
         val backupSets = getBackups(context, rootDir)
         val iterator = backupSets.iterator()
@@ -52,8 +54,7 @@ internal class DocumentsProviderRestorePlugin(
         }
     }
 
-    @WorkerThread
-    private fun getBackups(context: Context, rootDir: DocumentFile): List<BackupSet> {
+    private suspend fun getBackups(context: Context, rootDir: DocumentFile): List<BackupSet> {
         val backupSets = ArrayList<BackupSet>()
         val files = try {
             // block until the DocumentsProvider has results
@@ -76,7 +77,12 @@ internal class DocumentsProviderRestorePlugin(
                 continue
             }
             // block until children of set are available
-            val metadata = set.findFileBlocking(context, FILE_BACKUP_METADATA)
+            val metadata = try {
+                set.findFileBlocking(context, FILE_BACKUP_METADATA)
+            } catch (e: IOException) {
+                Log.e(TAG, "Error reading metadata file in backup set folder: ${set.name}", e)
+                null
+            }
             if (metadata == null) {
                 Log.w(TAG, "Missing metadata file in backup set folder: ${set.name}")
             } else {
@@ -87,7 +93,7 @@ internal class DocumentsProviderRestorePlugin(
     }
 
     @Throws(IOException::class)
-    override fun getApkInputStream(token: Long, packageName: String): InputStream {
+    override suspend fun getApkInputStream(token: Long, packageName: String): InputStream {
         val setDir = storage.getSetDir(token) ?: throw IOException()
         val file = setDir.findFile("$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 702aa429..d300efe3 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
@@ -1,10 +1,13 @@
+@file:Suppress("EXPERIMENTAL_API_USAGE", "BlockingMethodInNonBlockingContext")
+
 package com.stevesoltys.seedvault.plugins.saf
 
-import android.annotation.SuppressLint
 import android.content.Context
 import android.content.pm.PackageInfo
 import android.database.ContentObserver
+import android.database.Cursor
 import android.net.Uri
+import android.os.FileUtils.closeQuietly
 import android.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID
 import android.provider.DocumentsContract.Document.COLUMN_MIME_TYPE
 import android.provider.DocumentsContract.Document.MIME_TYPE_DIR
@@ -14,15 +17,19 @@ import android.provider.DocumentsContract.buildDocumentUriUsingTree
 import android.provider.DocumentsContract.buildTreeDocumentUri
 import android.provider.DocumentsContract.getDocumentId
 import android.util.Log
+import androidx.annotation.VisibleForTesting
 import androidx.documentfile.provider.DocumentFile
 import com.stevesoltys.seedvault.metadata.MetadataManager
 import com.stevesoltys.seedvault.settings.SettingsManager
 import com.stevesoltys.seedvault.settings.Storage
-import libcore.io.IoUtils.closeQuietly
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withTimeout
 import java.io.IOException
 import java.io.InputStream
 import java.io.OutputStream
-import java.util.concurrent.TimeUnit.MINUTES
+import kotlin.coroutines.resume
 
 const val DIRECTORY_ROOT = ".SeedVaultAndroidBackup"
 const val DIRECTORY_FULL_BACKUP = "full"
@@ -36,7 +43,10 @@ private val TAG = DocumentsStorage::class.java.simpleName
 internal class DocumentsStorage(
         private val context: Context,
         private val metadataManager: MetadataManager,
-        private val settingsManager: SettingsManager) {
+        private val settingsManager: SettingsManager
+) {
+
+    private val contentResolver = context.contentResolver
 
     internal var storage: Storage? = null
         get() {
@@ -45,20 +55,22 @@ internal class DocumentsStorage(
         }
 
     internal var rootBackupDir: DocumentFile? = null
-        get() {
+        get() = runBlocking {
             if (field == null) {
-                val parent = storage?.getDocumentFile(context) ?: return null
+                val parent = storage?.getDocumentFile(context)
+                        ?: return@runBlocking null
                 field = try {
-                    val rootDir = parent.createOrGetDirectory(DIRECTORY_ROOT)
-                    // create .nomedia file to prevent Android's MediaScanner from trying to index the backup
-                    rootDir.createOrGetFile(FILE_NO_MEDIA)
-                    rootDir
+                    parent.createOrGetDirectory(context, DIRECTORY_ROOT).apply {
+                        // create .nomedia file to prevent Android's MediaScanner
+                        // from trying to index the backup
+                        createOrGetFile(context, FILE_NO_MEDIA)
+                    }
                 } catch (e: IOException) {
                     Log.e(TAG, "Error creating root backup dir.", e)
                     null
                 }
             }
-            return field
+            field
         }
 
     private var currentToken: Long = 0L
@@ -68,47 +80,47 @@ internal class DocumentsStorage(
         }
 
     private var currentSetDir: DocumentFile? = null
-        get() {
+        get() = runBlocking {
             if (field == null) {
-                if (currentToken == 0L) return null
+                if (currentToken == 0L) return@runBlocking null
                 field = try {
-                    rootBackupDir?.createOrGetDirectory(currentToken.toString())
+                    rootBackupDir?.createOrGetDirectory(context, currentToken.toString())
                 } catch (e: IOException) {
                     Log.e(TAG, "Error creating current restore set dir.", e)
                     null
                 }
             }
-            return field
+            field
         }
 
     var currentFullBackupDir: DocumentFile? = null
-        get() {
+        get() = runBlocking {
             if (field == null) {
                 field = try {
-                    currentSetDir?.createOrGetDirectory(DIRECTORY_FULL_BACKUP)
+                    currentSetDir?.createOrGetDirectory(context, DIRECTORY_FULL_BACKUP)
                 } catch (e: IOException) {
                     Log.e(TAG, "Error creating full backup dir.", e)
                     null
                 }
             }
-            return field
+            field
         }
 
     var currentKvBackupDir: DocumentFile? = null
-        get() {
+        get() = runBlocking {
             if (field == null) {
                 field = try {
-                    currentSetDir?.createOrGetDirectory(DIRECTORY_KEY_VALUE_BACKUP)
+                    currentSetDir?.createOrGetDirectory(context, DIRECTORY_KEY_VALUE_BACKUP)
                 } catch (e: IOException) {
                     Log.e(TAG, "Error creating K/V backup dir.", e)
                     null
                 }
             }
-            return field
+            field
         }
 
     fun isInitialized(): Boolean {
-        if (settingsManager.getAndResetIsStorageChanging()) return false  // storage location has changed
+        if (settingsManager.getAndResetIsStorageChanging()) return false // storage location has changed
         val kvEmpty = currentKvBackupDir?.listFiles()?.isEmpty() ?: false
         val fullEmpty = currentFullBackupDir?.listFiles()?.isEmpty() ?: false
         return kvEmpty && fullEmpty
@@ -125,48 +137,61 @@ internal class DocumentsStorage(
 
     fun getAuthority(): String? = storage?.uri?.authority
 
-    fun getSetDir(token: Long = currentToken): DocumentFile? {
+    @Throws(IOException::class)
+    suspend fun getSetDir(token: Long = currentToken): DocumentFile? {
         if (token == currentToken) return currentSetDir
-        return rootBackupDir?.findFile(token.toString())
-    }
-
-    fun getKVBackupDir(token: Long = currentToken): DocumentFile? {
-        if (token == currentToken) return currentKvBackupDir ?: throw IOException()
-        return getSetDir(token)?.findFile(DIRECTORY_KEY_VALUE_BACKUP)
+        return rootBackupDir?.findFileBlocking(context, token.toString())
     }
 
     @Throws(IOException::class)
-    fun getOrCreateKVBackupDir(token: Long = currentToken): DocumentFile {
+    suspend fun getKVBackupDir(token: Long = currentToken): DocumentFile? {
         if (token == currentToken) return currentKvBackupDir ?: throw IOException()
-        val setDir = getSetDir(token) ?: throw IOException()
-        return setDir.createOrGetDirectory(DIRECTORY_KEY_VALUE_BACKUP)
+        return getSetDir(token)?.findFileBlocking(context, DIRECTORY_KEY_VALUE_BACKUP)
     }
 
-    fun getFullBackupDir(token: Long = currentToken): DocumentFile? {
+    @Throws(IOException::class)
+    suspend fun getOrCreateKVBackupDir(token: Long = currentToken): DocumentFile {
+        if (token == currentToken) return currentKvBackupDir ?: throw IOException()
+        val setDir = getSetDir(token) ?: throw IOException()
+        return setDir.createOrGetDirectory(context, DIRECTORY_KEY_VALUE_BACKUP)
+    }
+
+    @Throws(IOException::class)
+    suspend fun getFullBackupDir(token: Long = currentToken): DocumentFile? {
         if (token == currentToken) return currentFullBackupDir ?: throw IOException()
-        return getSetDir(token)?.findFile(DIRECTORY_FULL_BACKUP)
+        return getSetDir(token)?.findFileBlocking(context, DIRECTORY_FULL_BACKUP)
     }
 
     @Throws(IOException::class)
     fun getInputStream(file: DocumentFile): InputStream {
-        return context.contentResolver.openInputStream(file.uri) ?: throw IOException()
+        return contentResolver.openInputStream(file.uri) ?: throw IOException()
     }
 
     @Throws(IOException::class)
     fun getOutputStream(file: DocumentFile): OutputStream {
-        return context.contentResolver.openOutputStream(file.uri, "wt") ?: throw IOException()
+        return contentResolver.openOutputStream(file.uri, "wt") ?: throw IOException()
     }
 
 }
 
+/**
+ * Checks if a file exists and if not, creates it.
+ *
+ * If we were trying to create it right away, some providers create "filename (1)".
+ */
 @Throws(IOException::class)
-fun DocumentFile.createOrGetFile(name: String, mimeType: String = MIME_TYPE): DocumentFile {
-    return findFile(name) ?: createFile(mimeType, name) ?: throw IOException()
+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()
 }
 
+/**
+ * Checks if a directory already exists and if not, creates it.
+ */
 @Throws(IOException::class)
-fun DocumentFile.createOrGetDirectory(name: String): DocumentFile {
-    return findFile(name) ?: createDirectory(name) ?: throw IOException()
+suspend fun DocumentFile.createOrGetDirectory(context: Context, name: String): DocumentFile {
+    return findFileBlocking(context, name) ?: createDirectory(name) ?: throw IOException()
 }
 
 @Throws(IOException::class)
@@ -183,43 +208,22 @@ fun DocumentFile.assertRightFile(packageInfo: PackageInfo) {
  * This prevents getting an empty list even though there are children to be listed.
  */
 @Throws(IOException::class)
-fun DocumentFile.listFilesBlocking(context: Context): ArrayList<DocumentFile> {
+suspend fun DocumentFile.listFilesBlocking(context: Context): ArrayList<DocumentFile> {
     val resolver = context.contentResolver
     val childrenUri = buildChildDocumentsUriUsingTree(uri, getDocumentId(uri))
     val projection = arrayOf(COLUMN_DOCUMENT_ID, COLUMN_MIME_TYPE)
     val result = ArrayList<DocumentFile>()
 
-    @SuppressLint("Recycle")  // gets closed in with(), only earlier exit when null
-    var cursor = resolver.query(childrenUri, projection, null, null, null)
-            ?: throw IOException()
-    val loading = cursor.extras.getBoolean(EXTRA_LOADING, false)
-    if (loading) {
-        Log.d(TAG, "Wait for children to get loaded...")
-        var loaded = false
-        cursor.registerContentObserver(object : ContentObserver(null) {
-            override fun onChange(selfChange: Boolean, uri: Uri?) {
-                Log.d(TAG, "Children loaded. Continue...")
-                loaded = true
-            }
-        })
-        val timeout = MINUTES.toMillis(2)
-        var time = 0
-        // TODO replace loop with callback flow or something similar
-        while (!loaded && time < timeout) {
-            Thread.sleep(50)
-            time += 50
+    try {
+        getLoadedCursor {
+            resolver.query(childrenUri, projection, null, null, null)
         }
-        if (time >= timeout) Log.w(TAG, "Timed out while waiting for children to load")
-        closeQuietly(cursor)
-        // do a new query after content was loaded
-        @SuppressLint("Recycle")  // gets closed after with block
-        cursor = resolver.query(childrenUri, projection, null, null, null)
-                ?: throw IOException()
-    }
-    with(cursor) {
-        while (moveToNext()) {
-            val documentId = getString(0)
-            val isDirectory = getString(1) == MIME_TYPE_DIR
+    } catch (e: TimeoutCancellationException) {
+        throw IOException(e)
+    }.use { cursor ->
+        while (cursor.moveToNext()) {
+            val documentId = cursor.getString(0)
+            val isDirectory = cursor.getString(1) == MIME_TYPE_DIR
             val file = if (isDirectory) {
                 val treeUri = buildTreeDocumentUri(uri.authority, documentId)
                 DocumentFile.fromTreeUri(context, treeUri)!!
@@ -233,7 +237,14 @@ fun DocumentFile.listFilesBlocking(context: Context): ArrayList<DocumentFile> {
     return result
 }
 
-fun DocumentFile.findFileBlocking(context: Context, displayName: String): DocumentFile? {
+/**
+ * Same as [DocumentFile.findFile] only that it re-queries when the first result was stale.
+ *
+ * Most documents providers including Nextcloud are listing the full directory content
+ * when querying for a specific file in a directory,
+ * so there is no point in trying to optimize the query by not listing all children.
+ */
+suspend fun DocumentFile.findFileBlocking(context: Context, displayName: String): DocumentFile? {
     val files = try {
         listFilesBlocking(context)
     } catch (e: IOException) {
@@ -245,3 +256,45 @@ fun DocumentFile.findFileBlocking(context: Context, displayName: String): Docume
     }
     return null
 }
+
+/**
+ * Returns a cursor for the given query while ensuring that the cursor was loaded.
+ *
+ * When the SAF backend is a cloud storage provider (e.g. Nextcloud),
+ * it can happen that the query returns an outdated (e.g. empty) cursor
+ * which will only be updated in response to this query.
+ *
+ * See: https://commonsware.com/blog/2019/12/14/scoped-storage-stories-listfiles-woe.html
+ *
+ * This method uses a [suspendCancellableCoroutine] to wait for the result of a [ContentObserver]
+ * registered on the cursor in case the cursor is still loading ([EXTRA_LOADING]).
+ * If the cursor is not loading, it will be returned right away.
+ *
+ * @param timeout an optional time-out in milliseconds
+ * @throws TimeoutCancellationException if there was no result before the time-out
+ * @throws IOException if the query returns null
+ */
+@VisibleForTesting
+@Throws(IOException::class, TimeoutCancellationException::class)
+internal suspend fun getLoadedCursor(timeout: Long = 15_000, query: () -> Cursor?) = withTimeout(timeout) {
+    suspendCancellableCoroutine<Cursor> { 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/restore/RestoreViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt
index fbc414ca..ec688cb9 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt
@@ -296,7 +296,8 @@ internal class RestoreViewModel(
                             }
                         }
                     }
-                    RestoreSetResult(restorableBackups)
+                    if (restorableBackups.isEmpty()) RestoreSetResult(app.getString(R.string.restore_set_empty_result))
+                    else RestoreSetResult(restorableBackups)
                 }
             }
             continuation.resume(result)
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 06cd2745..490bb8d4 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransport.kt
@@ -12,6 +12,7 @@ import android.util.Log
 import com.stevesoltys.seedvault.settings.SettingsActivity
 import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
 import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
+import kotlinx.coroutines.runBlocking
 import org.koin.core.KoinComponent
 import org.koin.core.inject
 
@@ -57,24 +58,24 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
     // General backup methods
     //
 
-    override fun initializeDevice(): Int {
-        return backupCoordinator.initializeDevice()
+    override fun initializeDevice(): Int = runBlocking {
+        backupCoordinator.initializeDevice()
     }
 
     override fun isAppEligibleForBackup(targetPackage: PackageInfo, isFullBackup: Boolean): Boolean {
         return backupCoordinator.isAppEligibleForBackup(targetPackage, isFullBackup)
     }
 
-    override fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long {
-        return backupCoordinator.getBackupQuota(packageName, isFullBackup)
+    override fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long = runBlocking {
+        backupCoordinator.getBackupQuota(packageName, isFullBackup)
     }
 
     override fun clearBackupData(packageInfo: PackageInfo): Int {
         return backupCoordinator.clearBackupData(packageInfo)
     }
 
-    override fun finishBackup(): Int {
-        return backupCoordinator.finishBackup()
+    override fun finishBackup(): Int = runBlocking {
+        backupCoordinator.finishBackup()
     }
 
     // ------------------------------------------------------------------------------------
@@ -85,8 +86,8 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
         return backupCoordinator.requestBackupTime()
     }
 
-    override fun performBackup(packageInfo: PackageInfo, inFd: ParcelFileDescriptor, flags: Int): Int {
-        return backupCoordinator.performIncrementalBackup(packageInfo, inFd, flags)
+    override fun performBackup(packageInfo: PackageInfo, inFd: ParcelFileDescriptor, flags: Int): Int = runBlocking {
+        backupCoordinator.performIncrementalBackup(packageInfo, inFd, flags)
     }
 
     override fun performBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor): Int {
@@ -106,20 +107,20 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
         return backupCoordinator.checkFullBackupSize(size)
     }
 
-    override fun performFullBackup(targetPackage: PackageInfo, socket: ParcelFileDescriptor, flags: Int): Int {
-        return backupCoordinator.performFullBackup(targetPackage, socket, flags)
+    override fun performFullBackup(targetPackage: PackageInfo, socket: ParcelFileDescriptor, flags: Int): Int = runBlocking {
+        backupCoordinator.performFullBackup(targetPackage, socket, flags)
     }
 
-    override fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor): Int {
+    override fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor): Int = runBlocking {
         Log.w(TAG, "Warning: Legacy performFullBackup() method called.")
-        return backupCoordinator.performFullBackup(targetPackage, fileDescriptor, 0)
+        backupCoordinator.performFullBackup(targetPackage, fileDescriptor, 0)
     }
 
-    override fun sendBackupData(numBytes: Int): Int {
-        return backupCoordinator.sendBackupData(numBytes)
+    override fun sendBackupData(numBytes: Int): Int = runBlocking {
+        backupCoordinator.sendBackupData(numBytes)
     }
 
-    override fun cancelFullBackup() {
+    override fun cancelFullBackup() = runBlocking {
         backupCoordinator.cancelFullBackup()
     }
 
@@ -127,8 +128,8 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
     // Restore
     //
 
-    override fun getAvailableRestoreSets(): Array<RestoreSet>? {
-        return restoreCoordinator.getAvailableRestoreSets()
+    override fun getAvailableRestoreSets(): Array<RestoreSet>? = runBlocking {
+        restoreCoordinator.getAvailableRestoreSets()
     }
 
     override fun getCurrentRestoreSet(): Long {
@@ -139,12 +140,12 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
         return restoreCoordinator.startRestore(token, packages)
     }
 
-    override fun getNextFullRestoreDataChunk(socket: ParcelFileDescriptor): Int {
-        return restoreCoordinator.getNextFullRestoreDataChunk(socket)
+    override fun getNextFullRestoreDataChunk(socket: ParcelFileDescriptor): Int = runBlocking {
+        restoreCoordinator.getNextFullRestoreDataChunk(socket)
     }
 
-    override fun nextRestorePackage(): RestoreDescription? {
-        return restoreCoordinator.nextRestorePackage()
+    override fun nextRestorePackage(): RestoreDescription? = runBlocking {
+        restoreCoordinator.nextRestorePackage()
     }
 
     override fun getRestoreData(outputFileDescriptor: ParcelFileDescriptor): Int {
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/ApkBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/ApkBackup.kt
index 4ab5f396..119db711 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/ApkBackup.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/ApkBackup.kt
@@ -36,7 +36,7 @@ class ApkBackup(
      * @return new [PackageMetadata] if an APK backup was made or null if no backup was made.
      */
     @Throws(IOException::class)
-    fun backupApkIfNecessary(packageInfo: PackageInfo, packageState: PackageState, streamGetter: () -> OutputStream): PackageMetadata? {
+    suspend fun backupApkIfNecessary(packageInfo: PackageInfo, packageState: PackageState, streamGetter: suspend () -> OutputStream): PackageMetadata? {
         // do not back up @pm@
         val packageName = packageInfo.packageName
         if (packageName == MAGIC_PACKAGE_MANAGER) return 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 e6df7cf1..f3ff72e9 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
@@ -9,6 +9,7 @@ import android.content.pm.PackageInfo
 import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
 import android.os.ParcelFileDescriptor
 import android.util.Log
+import androidx.annotation.WorkerThread
 import com.stevesoltys.seedvault.BackupNotificationManager
 import com.stevesoltys.seedvault.Clock
 import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
@@ -29,6 +30,8 @@ private val TAG = BackupCoordinator::class.java.simpleName
  * @author Steve Soltys
  * @author Torsten Grote
  */
+@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,
@@ -67,7 +70,7 @@ internal class BackupCoordinator(
      * @return One of [TRANSPORT_OK] (OK so far) or
      * [TRANSPORT_ERROR] (to retry following network error or other failure).
      */
-    fun initializeDevice(): Int {
+    suspend fun initializeDevice(): Int {
         Log.i(TAG, "Initialize Device!")
         return try {
             val token = clock.time()
@@ -107,7 +110,7 @@ internal class BackupCoordinator(
      *                      otherwise for key-value backup.
      * @return Current limit on backup size in bytes.
      */
-    fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long {
+    suspend fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long {
         if (packageName != MAGIC_PACKAGE_MANAGER) {
             // try to back up APK here as later methods are sometimes not called called
             backUpApk(context.packageManager.getPackageInfo(packageName, GET_SIGNING_CERTIFICATES))
@@ -139,7 +142,7 @@ internal class BackupCoordinator(
         Log.i(TAG, "Request incremental backup time. Returned $this")
     }
 
-    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) {
@@ -182,12 +185,12 @@ internal class BackupCoordinator(
         return result
     }
 
-    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)
     }
 
-    fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes)
+    suspend fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes)
 
     /**
      * Tells the transport to cancel the currently-ongoing full backup operation.
@@ -202,7 +205,7 @@ internal class BackupCoordinator(
      * If the transport receives this callback, it will *not* receive a call to [finishBackup].
      * It needs to tear down any ongoing backup state here.
      */
-    fun cancelFullBackup() {
+    suspend fun cancelFullBackup() {
         val packageInfo = full.getCurrentPackage()
                 ?: throw AssertionError("Cancelling full backup, but no current package")
         Log.i(TAG, "Cancel full backup of ${packageInfo.packageName} because of $cancelReason")
@@ -248,7 +251,7 @@ internal class BackupCoordinator(
      *
      * @return the same error codes as [performIncrementalBackup] or [performFullBackup].
      */
-    fun finishBackup(): Int = when {
+    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
@@ -267,7 +270,7 @@ internal class BackupCoordinator(
         else -> throw IllegalStateException("Unexpected state in finishBackup()")
     }
 
-    private fun backUpNotAllowedPackages() {
+    private suspend fun backUpNotAllowedPackages() {
         Log.d(TAG, "Checking if APKs of opt-out apps need backup...")
         packageService.notAllowedPackages.forEach { optOutPackageInfo ->
             try {
@@ -278,7 +281,7 @@ internal class BackupCoordinator(
         }
     }
 
-    private 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) {
@@ -292,7 +295,7 @@ internal class BackupCoordinator(
         }
     }
 
-    private fun onPackageBackedUp(packageInfo: PackageInfo) {
+    private suspend fun onPackageBackedUp(packageInfo: PackageInfo) {
         val packageName = packageInfo.packageName
         try {
             val outputStream = plugin.getMetadataOutputStream()
@@ -302,7 +305,7 @@ internal class BackupCoordinator(
         }
     }
 
-    private fun onPackageBackupError(packageInfo: PackageInfo) {
+    private suspend fun onPackageBackupError(packageInfo: PackageInfo) {
         // don't bother with system apps that have no data
         if (cancelReason == NO_DATA && packageInfo.isSystemApp()) return
         val packageName = packageInfo.packageName
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupPlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupPlugin.kt
index 9b01d0b9..c8d36a58 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupPlugin.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupPlugin.kt
@@ -17,19 +17,19 @@ interface BackupPlugin {
      * false if the device was initialized already and initialization should be a no-op.
      */
     @Throws(IOException::class)
-    fun initializeDevice(newToken: Long): Boolean
+    suspend fun initializeDevice(newToken: Long): Boolean
 
     /**
      * Returns an [OutputStream] for writing backup metadata.
      */
     @Throws(IOException::class)
-    fun getMetadataOutputStream(): OutputStream
+    suspend fun getMetadataOutputStream(): OutputStream
 
     /**
      * Returns an [OutputStream] for writing an APK to be backed up.
      */
     @Throws(IOException::class)
-    fun getApkOutputStream(packageInfo: PackageInfo): OutputStream
+    suspend fun getApkOutputStream(packageInfo: PackageInfo): OutputStream
 
     /**
      * Returns the package name of the app that provides the backend storage
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 0782bcbe..15b84089 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
@@ -21,7 +21,7 @@ private class FullBackupState(
         internal val packageInfo: PackageInfo,
         internal val inputFileDescriptor: ParcelFileDescriptor,
         internal val inputStream: InputStream,
-        internal var outputStreamInit: (() -> OutputStream)?) {
+        internal var outputStreamInit: (suspend () -> OutputStream)?) {
     internal var outputStream: OutputStream? = null
     internal val packageName: String = packageInfo.packageName
     internal var size: Long = 0
@@ -31,6 +31,7 @@ const val DEFAULT_QUOTA_FULL_BACKUP = (2 * (25 * 1024 * 1024)).toLong()
 
 private val TAG = FullBackup::class.java.simpleName
 
+@Suppress("BlockingMethodInNonBlockingContext")
 internal class FullBackup(
         private val plugin: FullBackupPlugin,
         private val inputFactory: InputFactory,
@@ -89,7 +90,7 @@ 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.
      */
-    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}.")
 
@@ -119,7 +120,7 @@ internal class FullBackup(
         return TRANSPORT_OK
     }
 
-    fun sendBackupData(numBytes: Int): Int {
+    suspend fun sendBackupData(numBytes: Int): Int {
         val state = this.state
                 ?: throw AssertionError("Attempted sendBackupData before performFullBackup")
 
@@ -134,11 +135,11 @@ internal class FullBackup(
         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" }
-            val outputStream = state.outputStream ?: {
-                val stream = state.outputStreamInit!!.invoke()  // not-null due to check above
+            val outputStream = state.outputStream ?: suspend {
+                val stream = state.outputStreamInit!!()  // not-null due to check above
                 state.outputStream = stream
                 stream
-            }.invoke()
+            }()
             state.outputStreamInit = null  // the stream init lambda is not needed beyond that point
 
             // read backup data, encrypt it and write it to output stream
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 e4dc6538..e07fbff7 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
@@ -10,7 +10,7 @@ interface FullBackupPlugin {
 
     // TODO consider using a salted hash for the package name to not leak it to the storage server
     @Throws(IOException::class)
-    fun getOutputStream(targetPackage: PackageInfo): OutputStream
+    suspend fun getOutputStream(targetPackage: PackageInfo): OutputStream
 
     /**
      * Remove all data associated with the given package.
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 8c1dceff..af14e1ec 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
@@ -21,6 +21,7 @@ const val DEFAULT_QUOTA_KEY_VALUE_BACKUP = (2 * (5 * 1024 * 1024)).toLong()
 
 private val TAG = KVBackup::class.java.simpleName
 
+@Suppress("BlockingMethodInNonBlockingContext")
 internal class KVBackup(
         private val plugin: KVBackupPlugin,
         private val inputFactory: InputFactory,
@@ -35,7 +36,7 @@ internal class KVBackup(
 
     fun getQuota(): Long = plugin.getQuota()
 
-    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
@@ -91,7 +92,7 @@ internal class KVBackup(
         return storeRecords(packageInfo, data)
     }
 
-    private fun storeRecords(packageInfo: PackageInfo, data: ParcelFileDescriptor): Int {
+    private suspend fun storeRecords(packageInfo: PackageInfo, data: ParcelFileDescriptor): Int {
         // apply the delta operations
         for (result in parseBackupStream(data)) {
             if (result is Result.Error) {
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 fb6fa64d..416ed978 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
@@ -25,14 +25,14 @@ interface KVBackupPlugin {
      * E.g. file-based plugins should a create a directory for the package, if none exists.
      */
     @Throws(IOException::class)
-    fun ensureRecordStorageForPackage(packageInfo: PackageInfo)
+    suspend fun ensureRecordStorageForPackage(packageInfo: PackageInfo)
 
     /**
      * Return an [OutputStream] for the given package and key
      * which will receive the record's encrypted value.
      */
     @Throws(IOException::class)
-    fun getOutputStreamForRecord(packageInfo: PackageInfo, key: String): OutputStream
+    suspend fun getOutputStreamForRecord(packageInfo: PackageInfo, key: String): OutputStream
 
     /**
      * Delete the record for the given package identified by the given key.
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt
index a233a490..bc26ba19 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt
@@ -23,6 +23,7 @@ private class FullRestoreState(
 
 private val TAG = FullRestore::class.java.simpleName
 
+@Suppress("BlockingMethodInNonBlockingContext")
 internal class FullRestore(
         private val plugin: FullRestorePlugin,
         private val outputFactory: OutputFactory,
@@ -37,7 +38,7 @@ internal class FullRestore(
      * Return true if there is data stored for the given package.
      */
     @Throws(IOException::class)
-    fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean {
+    suspend fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean {
         return plugin.hasDataForPackage(token, packageInfo)
     }
 
@@ -78,7 +79,7 @@ internal class FullRestore(
      * Any other negative value such as [TRANSPORT_ERROR] is treated as a fatal error condition
      * that aborts all further restore operations on the current dataset.
      */
-    fun getNextFullRestoreDataChunk(socket: ParcelFileDescriptor): Int {
+    suspend fun getNextFullRestoreDataChunk(socket: ParcelFileDescriptor): Int {
         val state = this.state ?: throw IllegalStateException("no state")
         val packageName = state.packageInfo.packageName
 
@@ -113,6 +114,7 @@ internal class FullRestore(
         try {
             // read segment from input stream and decrypt it
             val decrypted = try {
+                // TODO handle IOException
                 crypto.decryptSegment(inputStream)
             } catch (e: EOFException) {
                 Log.i(TAG, "   EOF")
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestorePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestorePlugin.kt
index 4fff7efd..dacd0e0b 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestorePlugin.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestorePlugin.kt
@@ -10,9 +10,9 @@ interface FullRestorePlugin {
      * Return true if there is data stored for the given package.
      */
     @Throws(IOException::class)
-    fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean
+    suspend fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean
 
     @Throws(IOException::class)
-    fun getInputStreamForPackage(token: Long, packageInfo: PackageInfo): InputStream
+    suspend fun getInputStreamForPackage(token: Long, packageInfo: PackageInfo): InputStream
 
 }
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 38c5d8b4..ad0dfcc5 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
@@ -40,7 +40,7 @@ internal class KVRestore(
      * Return true if there are records stored for the given package.
      */
     @Throws(IOException::class)
-    fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean {
+    suspend fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean {
         return plugin.hasDataForPackage(token, packageInfo)
     }
 
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 4769e3e5..a79d20ea 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
@@ -10,7 +10,7 @@ interface KVRestorePlugin {
      * Return true if there is data stored for the given package.
      */
     @Throws(IOException::class)
-    fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean
+    suspend fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean
 
     /**
      * Return all record keys for the given token and package.
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 a13a6822..78100f3f 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
@@ -37,6 +37,7 @@ private class RestoreCoordinatorState(
 
 private val TAG = RestoreCoordinator::class.java.simpleName
 
+@Suppress("BlockingMethodInNonBlockingContext")
 internal class RestoreCoordinator(
         private val context: Context,
         private val settingsManager: SettingsManager,
@@ -57,7 +58,7 @@ internal class RestoreCoordinator(
      * @return Descriptions of the set of restore images available for this device,
      *   or null if an error occurred (the attempt should be rescheduled).
      **/
-    fun getAvailableRestoreSets(): Array<RestoreSet>? {
+    suspend fun getAvailableRestoreSets(): Array<RestoreSet>? {
         val availableBackups = plugin.getAvailableBackups() ?: return null
         val restoreSets = ArrayList<RestoreSet>()
         val metadataMap = LongSparseArray<BackupMetadata>()
@@ -169,7 +170,7 @@ internal class RestoreCoordinator(
      * or [NO_MORE_PACKAGES] to indicate that no more packages can be restored in this session;
      * or null to indicate a transport-level error.
      */
-    fun nextRestorePackage(): RestoreDescription? {
+    suspend fun nextRestorePackage(): RestoreDescription? {
         Log.i(TAG, "Next restore package!")
         val state = this.state ?: throw IllegalStateException("no state")
 
@@ -228,7 +229,7 @@ internal class RestoreCoordinator(
      * After this method returns zero, the system will then call [nextRestorePackage]
      * to begin the restore process for the next application, and the sequence begins again.
      */
-    fun getNextFullRestoreDataChunk(outputFileDescriptor: ParcelFileDescriptor): Int {
+    suspend fun getNextFullRestoreDataChunk(outputFileDescriptor: ParcelFileDescriptor): Int {
         return full.getNextFullRestoreDataChunk(outputFileDescriptor)
     }
 
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestorePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestorePlugin.kt
index 750c9b11..91843998 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestorePlugin.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestorePlugin.kt
@@ -18,7 +18,7 @@ interface RestorePlugin {
      * @return metadata for the set of restore images available,
      * or null if an error occurred (the attempt should be rescheduled).
      **/
-    fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>?
+    suspend fun getAvailableBackups(): Sequence<EncryptedBackupMetadata>?
 
     /**
      * Searches if there's really a backup available in the given location.
@@ -27,12 +27,13 @@ interface RestorePlugin {
      * FIXME: Passing a Uri is maybe too plugin-specific?
      */
     @WorkerThread
-    fun hasBackup(uri: Uri): Boolean
+    @Throws(IOException::class)
+    suspend fun hasBackup(uri: Uri): Boolean
 
     /**
      * Returns an [InputStream] for the given token, for reading an APK that is to be restored.
      */
     @Throws(IOException::class)
-    fun getApkInputStream(token: Long, packageName: String): InputStream
+    suspend fun getApkInputStream(token: Long, packageName: String): InputStream
 
 }
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt
index 1df09664..c1372a9a 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt
@@ -3,10 +3,14 @@ package com.stevesoltys.seedvault.ui.storage
 import android.app.Application
 import android.net.Uri
 import android.util.Log
+import androidx.lifecycle.viewModelScope
 import com.stevesoltys.seedvault.R
 import com.stevesoltys.seedvault.plugins.saf.DIRECTORY_ROOT
 import com.stevesoltys.seedvault.settings.SettingsManager
 import com.stevesoltys.seedvault.transport.restore.RestorePlugin
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.io.IOException
 
 private val TAG = RestoreStorageViewModel::class.java.simpleName
 
@@ -17,18 +21,26 @@ internal class RestoreStorageViewModel(
 
     override val isRestoreOperation = true
 
-    override fun onLocationSet(uri: Uri) = Thread {
-        if (restorePlugin.hasBackup(uri)) {
-            saveStorage(uri)
+    override fun onLocationSet(uri: Uri) {
+        viewModelScope.launch(Dispatchers.IO) {
+            val hasBackup = try {
+                restorePlugin.hasBackup(uri)
+            } catch (e: IOException) {
+                Log.e(TAG, "Error reading URI: $uri", e)
+                false
+            }
+            if (hasBackup) {
+                saveStorage(uri)
 
-            mLocationChecked.postEvent(LocationResult())
-        } else {
-            Log.w(TAG, "Location was rejected: $uri")
+                mLocationChecked.postEvent(LocationResult())
+            } else {
+                Log.w(TAG, "Location was rejected: $uri")
 
-            // notify the UI that the location was invalid
-            val errorMsg = app.getString(R.string.restore_invalid_location_message, DIRECTORY_ROOT)
-            mLocationChecked.postEvent(LocationResult(errorMsg))
+                // notify the UI that the location was invalid
+                val errorMsg = app.getString(R.string.restore_invalid_location_message, DIRECTORY_ROOT)
+                mLocationChecked.postEvent(LocationResult(errorMsg))
+            }
         }
-    }.start()
+    }
 
 }
diff --git a/app/src/sharedTest/java/com/stevesoltys/seedvault/TestUtils.kt b/app/src/sharedTest/java/com/stevesoltys/seedvault/TestUtils.kt
index bc0e211b..dafb2c41 100644
--- a/app/src/sharedTest/java/com/stevesoltys/seedvault/TestUtils.kt
+++ b/app/src/sharedTest/java/com/stevesoltys/seedvault/TestUtils.kt
@@ -1,5 +1,11 @@
 package com.stevesoltys.seedvault
 
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
+import java.io.InputStream
+import java.io.OutputStream
 import kotlin.random.Random
 
 fun assertContains(stack: String?, needle: String) {
@@ -44,3 +50,24 @@ fun ByteArray.toIntString(): String {
     }
     return str
 }
+
+fun OutputStream.writeAndClose(data: ByteArray) = use {
+    it.write(data)
+}
+
+fun assertReadEquals(data: ByteArray, inputStream: InputStream?) = inputStream?.use {
+    assertArrayEquals(data, it.readBytes())
+} ?: error("no input stream")
+
+fun <T : Throwable> coAssertThrows(clazz: Class<T>, block: suspend () -> Unit) {
+    var thrown = false
+    try {
+        runBlocking {
+            block()
+        }
+    } catch (e: Throwable) {
+        assertEquals(clazz, e.javaClass)
+        thrown = true
+    }
+    if (!thrown) fail("Exception was not thrown: " + clazz.name)
+}
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 577dbd76..1b14fff7 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
@@ -37,9 +37,11 @@ import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
 import com.stevesoltys.seedvault.transport.restore.RestorePlugin
 import io.mockk.CapturingSlot
 import io.mockk.Runs
+import io.mockk.coEvery
 import io.mockk.every
 import io.mockk.just
 import io.mockk.mockk
+import kotlinx.coroutines.runBlocking
 import org.junit.jupiter.api.Assertions.assertArrayEquals
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Assertions.fail
@@ -48,6 +50,7 @@ import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
 import kotlin.random.Random
 
+@Suppress("BlockingMethodInNonBlockingContext")
 internal class CoordinatorIntegrationTest : TransportTest() {
 
     private val inputFactory = mockk<InputFactory>()
@@ -94,7 +97,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
     }
 
     @Test
-    fun `test key-value backup and restore with 2 records`() {
+    fun `test key-value backup and restore with 2 records`() = runBlocking {
         val value = CapturingSlot<ByteArray>()
         val value2 = CapturingSlot<ByteArray>()
         val bOutputStream = ByteArrayOutputStream()
@@ -102,7 +105,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
 
         // read one key/value record and write it to output stream
         every { kvBackupPlugin.hasDataForPackage(packageInfo) } returns false
-        every { kvBackupPlugin.ensureRecordStorageForPackage(packageInfo) } just Runs
+        coEvery { kvBackupPlugin.ensureRecordStorageForPackage(packageInfo) } just Runs
         every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput
         every { backupDataInput.readNextHeader() } returns true andThen true andThen false
         every { backupDataInput.key } returns key andThen key2
@@ -111,14 +114,14 @@ internal class CoordinatorIntegrationTest : TransportTest() {
             appData.copyInto(value.captured) // write the app data into the passed ByteArray
             appData.size
         }
-        every { 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
         }
-        every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key264) } returns bOutputStream2
-        every { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns packageMetadata
-        every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
+        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.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs
 
@@ -130,7 +133,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
         assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo)))
 
         // find data for K/V backup
-        every { kvRestorePlugin.hasDataForPackage(token, packageInfo) } returns true
+        coEvery { kvRestorePlugin.hasDataForPackage(token, packageInfo) } returns true
 
         val restoreDescription = restore.nextRestorePackage() ?: fail()
         assertEquals(packageInfo.packageName, restoreDescription.packageName)
@@ -153,7 +156,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
     }
 
     @Test
-    fun `test key-value backup with huge value`() {
+    fun `test key-value backup with huge value`() = runBlocking {
         val value = CapturingSlot<ByteArray>()
         val size = Random.nextInt(5) * MAX_SEGMENT_CLEARTEXT_LENGTH + Random.nextInt(0, 1337)
         val appData = ByteArray(size).apply { Random.nextBytes(this) }
@@ -161,7 +164,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
 
         // read one key/value record and write it to output stream
         every { kvBackupPlugin.hasDataForPackage(packageInfo) } returns false
-        every { kvBackupPlugin.ensureRecordStorageForPackage(packageInfo) } just Runs
+        coEvery { kvBackupPlugin.ensureRecordStorageForPackage(packageInfo) } just Runs
         every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput
         every { backupDataInput.readNextHeader() } returns true andThen false
         every { backupDataInput.key } returns key
@@ -170,9 +173,9 @@ internal class CoordinatorIntegrationTest : TransportTest() {
             appData.copyInto(value.captured) // write the app data into the passed ByteArray
             appData.size
         }
-        every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key64) } returns bOutputStream
-        every { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns null
-        every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
+        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
 
         // start and finish K/V backup
@@ -183,7 +186,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
         assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo)))
 
         // find data for K/V backup
-        every { kvRestorePlugin.hasDataForPackage(token, packageInfo) } returns true
+        coEvery { kvRestorePlugin.hasDataForPackage(token, packageInfo) } returns true
 
         val restoreDescription = restore.nextRestorePackage() ?: fail()
         assertEquals(packageInfo.packageName, restoreDescription.packageName)
@@ -202,15 +205,15 @@ internal class CoordinatorIntegrationTest : TransportTest() {
     }
 
     @Test
-    fun `test full backup and restore with two chunks`() {
+    fun `test full backup and restore with two chunks`() = runBlocking {
         // return streams from plugin and app data
         val bOutputStream = ByteArrayOutputStream()
         val bInputStream = ByteArrayInputStream(appData)
-        every { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream
+        coEvery { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream
         every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream
         every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
-        every { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns packageMetadata
-        every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
+        coEvery { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns packageMetadata
+        coEvery { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
         every { metadataManager.onApkBackedUp(packageInfo, packageMetadata, metadataOutputStream) } just Runs
         every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs
 
@@ -224,8 +227,8 @@ internal class CoordinatorIntegrationTest : TransportTest() {
         assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo)))
 
         // find data only for full backup
-        every { kvRestorePlugin.hasDataForPackage(token, packageInfo) } returns false
-        every { fullRestorePlugin.hasDataForPackage(token, packageInfo) } returns true
+        coEvery { kvRestorePlugin.hasDataForPackage(token, packageInfo) } returns false
+        coEvery { fullRestorePlugin.hasDataForPackage(token, packageInfo) } returns true
 
         val restoreDescription = restore.nextRestorePackage() ?: fail()
         assertEquals(packageInfo.packageName, restoreDescription.packageName)
@@ -234,7 +237,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
         // reverse the backup streams into restore input
         val rInputStream = ByteArrayInputStream(bOutputStream.toByteArray())
         val rOutputStream = ByteArrayOutputStream()
-        every { 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/ApkBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupTest.kt
index 7a4168de..3a863780 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupTest.kt
@@ -11,10 +11,12 @@ import com.stevesoltys.seedvault.getRandomString
 import com.stevesoltys.seedvault.metadata.PackageMetadata
 import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
 import io.mockk.Runs
+import io.mockk.coEvery
 import io.mockk.every
 import io.mockk.just
 import io.mockk.mockk
 import io.mockk.mockkStatic
+import kotlinx.coroutines.runBlocking
 import org.junit.jupiter.api.Assertions.assertArrayEquals
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Assertions.assertNull
@@ -30,10 +32,11 @@ import java.nio.file.Path
 import kotlin.random.Random
 
 
+@Suppress("BlockingMethodInNonBlockingContext")
 internal class ApkBackupTest : BackupTest() {
 
     private val pm: PackageManager = mockk()
-    private val streamGetter: () -> OutputStream = mockk()
+    private val streamGetter: suspend () -> OutputStream = mockk()
 
     private val apkBackup = ApkBackup(pm, settingsManager, metadataManager)
 
@@ -51,20 +54,20 @@ internal class ApkBackupTest : BackupTest() {
     }
 
     @Test
-    fun `does not back up @pm@`() {
+    fun `does not back up @pm@`() = runBlocking {
         val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
         assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
     }
 
     @Test
-    fun `does not back up when setting disabled`() {
+    fun `does not back up when setting disabled`() = runBlocking {
         every { settingsManager.backupApks() } returns false
 
         assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
     }
 
     @Test
-    fun `does not back up system apps`() {
+    fun `does not back up system apps`() = runBlocking {
         packageInfo.applicationInfo.flags = FLAG_SYSTEM
 
         every { settingsManager.backupApks() } returns true
@@ -73,7 +76,7 @@ internal class ApkBackupTest : BackupTest() {
     }
 
     @Test
-    fun `does not back up the same version`() {
+    fun `does not back up the same version`() = runBlocking {
         packageInfo.applicationInfo.flags = FLAG_UPDATED_SYSTEM_APP
         val packageMetadata = packageMetadata.copy(
                 version = packageInfo.longVersionCode
@@ -91,12 +94,14 @@ internal class ApkBackupTest : BackupTest() {
         expectChecks()
 
         assertThrows(IOException::class.java) {
-            assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
+            runBlocking {
+                assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
+            }
         }
     }
 
     @Test
-    fun `do not accept empty signature`() {
+    fun `do not accept empty signature`() = runBlocking {
         every { settingsManager.backupApks() } returns true
         every { metadataManager.getPackageMetadata(packageInfo.packageName) } returns packageMetadata
         every { sigInfo.hasMultipleSigners() } returns false
@@ -106,7 +111,7 @@ internal class ApkBackupTest : BackupTest() {
     }
 
     @Test
-    fun `test successful APK backup`(@TempDir tmpDir: Path) {
+    fun `test successful APK backup`(@TempDir tmpDir: Path) = runBlocking {
         val apkBytes = byteArrayOf(0x04, 0x05, 0x06)
         val tmpFile = File(tmpDir.toAbsolutePath().toString())
         packageInfo.applicationInfo.sourceDir = File(tmpFile, "test.apk").apply {
@@ -124,7 +129,7 @@ internal class ApkBackupTest : BackupTest() {
         )
 
         expectChecks()
-        every { streamGetter.invoke() } returns apkOutputStream
+        coEvery { streamGetter.invoke() } returns apkOutputStream
         every { pm.getInstallerPackageName(packageInfo.packageName) } returns updatedMetadata.installer
         every { metadataManager.onApkBackedUp(packageInfo, updatedMetadata, outputStream) } just Runs
 
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 4136e361..08566626 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
@@ -10,6 +10,7 @@ import android.os.ParcelFileDescriptor
 import androidx.documentfile.provider.DocumentFile
 import com.stevesoltys.seedvault.BackupNotificationManager
 import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
+import com.stevesoltys.seedvault.coAssertThrows
 import com.stevesoltys.seedvault.getRandomString
 import com.stevesoltys.seedvault.metadata.PackageMetadata
 import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
@@ -18,19 +19,22 @@ import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
 import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
 import com.stevesoltys.seedvault.settings.Storage
 import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.coVerify
 import io.mockk.every
 import io.mockk.just
 import io.mockk.mockk
 import io.mockk.verify
+import kotlinx.coroutines.runBlocking
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Assertions.assertFalse
-import org.junit.jupiter.api.Assertions.assertThrows
 import org.junit.jupiter.api.Assertions.assertTrue
 import org.junit.jupiter.api.Test
 import java.io.IOException
 import java.io.OutputStream
 import kotlin.random.Random
 
+@Suppress("BlockingMethodInNonBlockingContext")
 internal class BackupCoordinatorTest : BackupTest() {
 
     private val plugin = mockk<BackupPlugin>()
@@ -48,10 +52,10 @@ internal class BackupCoordinatorTest : BackupTest() {
     private val storage = Storage(Uri.EMPTY, getRandomString(), false)
 
     @Test
-    fun `device initialization succeeds and delegates to plugin`() {
+    fun `device initialization succeeds and delegates to plugin`() = runBlocking {
         every { clock.time() } returns token
-        every { plugin.initializeDevice(token) } returns true // TODO test when false
-        every { plugin.getMetadataOutputStream() } returns metadataOutputStream
+        coEvery { plugin.initializeDevice(token) } returns true // TODO test when false
+        coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream
         every { metadataManager.onDeviceInitialization(token, metadataOutputStream) } just Runs
         every { kv.hasState() } returns false
         every { full.hasState() } returns false
@@ -61,9 +65,9 @@ internal class BackupCoordinatorTest : BackupTest() {
     }
 
     @Test
-    fun `device initialization does no-op when already initialized`() {
+    fun `device initialization does no-op when already initialized`() = runBlocking {
         every { clock.time() } returns token
-        every { plugin.initializeDevice(token) } returns false
+        coEvery { plugin.initializeDevice(token) } returns false
         every { kv.hasState() } returns false
         every { full.hasState() } returns false
 
@@ -72,9 +76,9 @@ internal class BackupCoordinatorTest : BackupTest() {
     }
 
     @Test
-    fun `error notification when device initialization fails`() {
+    fun `error notification when device initialization fails`() = runBlocking {
         every { clock.time() } returns token
-        every { plugin.initializeDevice(token) } throws IOException()
+        coEvery { plugin.initializeDevice(token) } throws IOException()
         every { settingsManager.getStorage() } returns storage
         every { notificationManager.onBackupError() } just Runs
 
@@ -83,18 +87,18 @@ internal class BackupCoordinatorTest : BackupTest() {
         // finish will only be called when TRANSPORT_OK is returned, so it should throw
         every { kv.hasState() } returns false
         every { full.hasState() } returns false
-        assertThrows(IllegalStateException::class.java) {
+        coAssertThrows(IllegalStateException::class.java) {
             backup.finishBackup()
         }
     }
 
     @Test
-    fun `no error notification when device initialization fails on unplugged USB storage`() {
+    fun `no error notification when device initialization fails on unplugged USB storage`() = runBlocking {
         val storage = mockk<Storage>()
         val documentFile = mockk<DocumentFile>()
 
         every { clock.time() } returns token
-        every { plugin.initializeDevice(token) } throws IOException()
+        coEvery { plugin.initializeDevice(token) } throws IOException()
         every { settingsManager.getStorage() } returns storage
         every { storage.isUsb } returns true
         every { storage.getDocumentFile(context) } returns documentFile
@@ -105,13 +109,13 @@ internal class BackupCoordinatorTest : BackupTest() {
         // finish will only be called when TRANSPORT_OK is returned, so it should throw
         every { kv.hasState() } returns false
         every { full.hasState() } returns false
-        assertThrows(IllegalStateException::class.java) {
+        coAssertThrows(IllegalStateException::class.java) {
             backup.finishBackup()
         }
     }
 
     @Test
-    fun `getBackupQuota() delegates to right plugin`() {
+    fun `getBackupQuota() delegates to right plugin`() = runBlocking {
         val isFullBackup = Random.nextBoolean()
         val quota = Random.nextLong()
 
@@ -154,7 +158,7 @@ internal class BackupCoordinatorTest : BackupTest() {
     }
 
     @Test
-    fun `clearing backup data succeeds`() {
+    fun `clearing backup data succeeds`() = runBlocking {
         every { kv.clearBackupData(packageInfo) } just Runs
         every { full.clearBackupData(packageInfo) } just Runs
 
@@ -167,13 +171,13 @@ internal class BackupCoordinatorTest : BackupTest() {
     }
 
     @Test
-    fun `finish backup delegates to KV plugin if it has state`() {
+    fun `finish backup delegates to KV plugin if it has state`() = runBlocking {
         val result = Random.nextInt()
 
         every { kv.hasState() } returns true
         every { full.hasState() } returns false
         every { kv.getCurrentPackage() } returns packageInfo
-        every { plugin.getMetadataOutputStream() } returns metadataOutputStream
+        coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream
         every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs
         every { kv.finishBackup() } returns result
 
@@ -181,13 +185,13 @@ internal class BackupCoordinatorTest : BackupTest() {
     }
 
     @Test
-    fun `finish backup delegates to full plugin if it has state`() {
+    fun `finish backup delegates to full plugin if it has state`() = runBlocking {
         val result = Random.nextInt()
 
         every { kv.hasState() } returns false
         every { full.hasState() } returns true
         every { full.getCurrentPackage() } returns packageInfo
-        every { plugin.getMetadataOutputStream() } returns metadataOutputStream
+        coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream
         every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs
         every { full.finishBackup() } returns result
 
@@ -195,16 +199,16 @@ internal class BackupCoordinatorTest : BackupTest() {
     }
 
     @Test
-    fun `metadata does not get updated when no APK was backed up`() {
-        every { full.performFullBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
-        every { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns null
+    fun `metadata does not get updated when no APK was backed up`() = runBlocking {
+        coEvery { full.performFullBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
+        coEvery { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns null
 
         assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, fileDescriptor, 0))
     }
 
     @Test
-    fun `app exceeding quota gets cancelled and reason written to metadata`() {
-        every { full.performFullBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
+    fun `app exceeding quota gets cancelled and reason written to metadata`() = runBlocking {
+        coEvery { full.performFullBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
         expectApkBackupAndMetadataWrite()
         every { full.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
         every { full.checkFullBackupSize(DEFAULT_QUOTA_FULL_BACKUP + 1) } returns TRANSPORT_QUOTA_EXCEEDED
@@ -228,8 +232,8 @@ internal class BackupCoordinatorTest : BackupTest() {
     }
 
     @Test
-    fun `app with no data gets cancelled and reason written to metadata`() {
-        every { full.performFullBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
+    fun `app with no data gets cancelled and reason written to metadata`() = runBlocking {
+        coEvery { full.performFullBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
         expectApkBackupAndMetadataWrite()
         every { full.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
         every { full.checkFullBackupSize(0) } returns TRANSPORT_PACKAGE_REJECTED
@@ -252,7 +256,7 @@ internal class BackupCoordinatorTest : BackupTest() {
     }
 
     @Test
-    fun `not allowed apps get their APKs backed up during @pm@ backup`() {
+    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" },
@@ -263,26 +267,26 @@ internal class BackupCoordinatorTest : BackupTest() {
         every { settingsManager.getStorage() } returns storage  // to check for removable storage
         every { packageService.notAllowedPackages } returns notAllowedPackages
         // no backup needed
-        every { 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
-        every { apkBackup.backupApkIfNecessary(notAllowedPackages[1], NOT_ALLOWED, any()) } returns packageMetadata
-        every { plugin.getMetadataOutputStream() } returns metadataOutputStream
+        coEvery { apkBackup.backupApkIfNecessary(notAllowedPackages[1], NOT_ALLOWED, any()) } returns packageMetadata
+        coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream
         every { metadataManager.onApkBackedUp(notAllowedPackages[1], packageMetadata, metadataOutputStream) } just Runs
         // do actual @pm@ backup
-        every { kv.performBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
+        coEvery { kv.performBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
 
         assertEquals(TRANSPORT_OK,
                 backup.performIncrementalBackup(packageInfo, fileDescriptor, 0))
 
-        verify {
+        coVerify {
             apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any())
             apkBackup.backupApkIfNecessary(notAllowedPackages[1], NOT_ALLOWED, any())
         }
     }
 
     private fun expectApkBackupAndMetadataWrite() {
-        every { apkBackup.backupApkIfNecessary(any(), UNKNOWN_ERROR, any()) } returns packageMetadata
-        every { plugin.getMetadataOutputStream() } returns metadataOutputStream
+        coEvery { apkBackup.backupApkIfNecessary(any(), UNKNOWN_ERROR, any()) } returns packageMetadata
+        coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream
         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 91f3d1aa..58546342 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
@@ -5,9 +5,11 @@ import android.app.backup.BackupTransport.TRANSPORT_OK
 import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
 import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
 import io.mockk.Runs
+import io.mockk.coEvery
 import io.mockk.every
 import io.mockk.just
 import io.mockk.mockk
+import kotlinx.coroutines.runBlocking
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Assertions.assertFalse
 import org.junit.jupiter.api.Assertions.assertTrue
@@ -16,6 +18,7 @@ import java.io.FileInputStream
 import java.io.IOException
 import kotlin.random.Random
 
+@Suppress("BlockingMethodInNonBlockingContext")
 internal class FullBackupTest : BackupTest() {
 
     private val plugin = mockk<FullBackupPlugin>()
@@ -62,7 +65,7 @@ internal class FullBackupTest : BackupTest() {
     }
 
     @Test
-    fun `performFullBackup runs ok`() {
+    fun `performFullBackup runs ok`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
         expectClearState()
 
@@ -73,7 +76,7 @@ internal class FullBackupTest : BackupTest() {
     }
 
     @Test
-    fun `sendBackupData first call over quota`() {
+    fun `sendBackupData first call over quota`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         val numBytes = (quota + 1).toInt()
@@ -89,7 +92,7 @@ internal class FullBackupTest : BackupTest() {
     }
 
     @Test
-    fun `sendBackupData second call over quota`() {
+    fun `sendBackupData second call over quota`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         val numBytes1 = quota.toInt()
@@ -109,7 +112,7 @@ internal class FullBackupTest : BackupTest() {
     }
 
     @Test
-    fun `sendBackupData throws exception when reading from InputStream`() {
+    fun `sendBackupData throws exception when reading from InputStream`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         every { plugin.getQuota() } returns quota
@@ -125,11 +128,11 @@ internal class FullBackupTest : BackupTest() {
     }
 
     @Test
-    fun `sendBackupData throws exception when getting outputStream`() {
+    fun `sendBackupData throws exception when getting outputStream`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
 
         every { plugin.getQuota() } returns quota
-        every { plugin.getOutputStream(packageInfo) } throws IOException()
+        coEvery { plugin.getOutputStream(packageInfo) } throws IOException()
         expectClearState()
 
         assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, data))
@@ -141,11 +144,11 @@ internal class FullBackupTest : BackupTest() {
     }
 
     @Test
-    fun `sendBackupData throws exception when writing header`() {
+    fun `sendBackupData throws exception when writing header`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
 
         every { plugin.getQuota() } returns quota
-        every { plugin.getOutputStream(packageInfo) } returns outputStream
+        coEvery { plugin.getOutputStream(packageInfo) } returns outputStream
         every { inputFactory.getInputStream(data) } returns inputStream
         every { headerWriter.writeVersion(outputStream, header) } throws IOException()
         expectClearState()
@@ -159,7 +162,7 @@ internal class FullBackupTest : BackupTest() {
     }
 
     @Test
-    fun `sendBackupData throws exception when writing encrypted data to OutputStream`() {
+    fun `sendBackupData throws exception when writing encrypted data to OutputStream`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         every { plugin.getQuota() } returns quota
@@ -176,7 +179,7 @@ internal class FullBackupTest : BackupTest() {
     }
 
     @Test
-    fun `sendBackupData runs ok`() {
+    fun `sendBackupData runs ok`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         val numBytes1 = (quota / 2).toInt()
@@ -203,7 +206,7 @@ internal class FullBackupTest : BackupTest() {
     }
 
     @Test
-    fun `cancel full backup runs ok`() {
+    fun `cancel full backup runs ok`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         expectClearState()
@@ -216,7 +219,7 @@ internal class FullBackupTest : BackupTest() {
     }
 
     @Test
-    fun `cancel full backup ignores exception when calling plugin`() {
+    fun `cancel full backup ignores exception when calling plugin`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         expectClearState()
@@ -229,7 +232,7 @@ internal class FullBackupTest : BackupTest() {
     }
 
     @Test
-    fun `clearState throws exception when flushing OutputStream`() {
+    fun `clearState throws exception when flushing OutputStream`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         val numBytes = 42
@@ -245,7 +248,7 @@ internal class FullBackupTest : BackupTest() {
     }
 
     @Test
-    fun `clearState ignores exception when closing OutputStream`() {
+    fun `clearState ignores exception when closing OutputStream`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         every { outputStream.flush() } just Runs
@@ -260,7 +263,7 @@ internal class FullBackupTest : BackupTest() {
     }
 
     @Test
-    fun `clearState ignores exception when closing InputStream`() {
+    fun `clearState ignores exception when closing InputStream`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         every { outputStream.flush() } just Runs
@@ -275,7 +278,7 @@ internal class FullBackupTest : BackupTest() {
     }
 
     @Test
-    fun `clearState ignores exception when closing ParcelFileDescriptor`() {
+    fun `clearState ignores exception when closing ParcelFileDescriptor`() = runBlocking {
         every { inputFactory.getInputStream(data) } returns inputStream
         expectInitializeOutputStream()
         every { outputStream.flush() } just Runs
@@ -290,7 +293,7 @@ internal class FullBackupTest : BackupTest() {
     }
 
     private fun expectInitializeOutputStream() {
-        every { plugin.getOutputStream(packageInfo) } returns outputStream
+        coEvery { plugin.getOutputStream(packageInfo) } returns outputStream
         every { headerWriter.writeVersion(outputStream, header) } just Runs
         every { crypto.encryptHeader(outputStream, header) } just Runs
     }
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 66956d92..31a3de81 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
@@ -11,9 +11,11 @@ import com.stevesoltys.seedvault.getRandomString
 import com.stevesoltys.seedvault.header.MAX_KEY_LENGTH_SIZE
 import com.stevesoltys.seedvault.header.VersionHeader
 import io.mockk.Runs
+import io.mockk.coEvery
 import io.mockk.every
 import io.mockk.just
 import io.mockk.mockk
+import kotlinx.coroutines.runBlocking
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Assertions.assertFalse
 import org.junit.jupiter.api.Assertions.assertTrue
@@ -22,6 +24,7 @@ import java.io.IOException
 import java.util.*
 import kotlin.random.Random
 
+@Suppress("BlockingMethodInNonBlockingContext")
 internal class KVBackupTest : BackupTest() {
 
     private val plugin = mockk<KVBackupPlugin>()
@@ -40,7 +43,7 @@ internal class KVBackupTest : BackupTest() {
     }
 
     @Test
-    fun `simple backup with one record`() {
+    fun `simple backup with one record`() = runBlocking {
         singleRecordBackup()
 
         assertEquals(TRANSPORT_OK, backup.performBackup(packageInfo, data, 0))
@@ -50,7 +53,7 @@ internal class KVBackupTest : BackupTest() {
     }
 
     @Test
-    fun `incremental backup with no data gets rejected`() {
+    fun `incremental backup with no data gets rejected`() = runBlocking {
         every { plugin.hasDataForPackage(packageInfo) } returns false
 
         assertEquals(TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED, backup.performBackup(packageInfo, data, FLAG_INCREMENTAL))
@@ -58,7 +61,7 @@ internal class KVBackupTest : BackupTest() {
     }
 
     @Test
-    fun `check for existing data throws exception`() {
+    fun `check for existing data throws exception`() = runBlocking {
         every { plugin.hasDataForPackage(packageInfo) } throws IOException()
 
         assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0))
@@ -66,7 +69,7 @@ internal class KVBackupTest : BackupTest() {
     }
 
     @Test
-    fun `non-incremental backup with data clears old data first`() {
+    fun `non-incremental backup with data clears old data first`() = runBlocking {
         singleRecordBackup(true)
         every { plugin.removeDataOfPackage(packageInfo) } just Runs
 
@@ -77,7 +80,7 @@ internal class KVBackupTest : BackupTest() {
     }
 
     @Test
-    fun `ignoring exception when clearing data when non-incremental backup has data`() {
+    fun `ignoring exception when clearing data when non-incremental backup has data`() = runBlocking {
         singleRecordBackup(true)
         every { plugin.removeDataOfPackage(packageInfo) } throws IOException()
 
@@ -88,16 +91,16 @@ internal class KVBackupTest : BackupTest() {
     }
 
     @Test
-    fun `ensuring storage throws exception`() {
+    fun `ensuring storage throws exception`() = runBlocking {
         every { plugin.hasDataForPackage(packageInfo) } returns false
-        every { plugin.ensureRecordStorageForPackage(packageInfo) } throws IOException()
+        coEvery { plugin.ensureRecordStorageForPackage(packageInfo) } throws IOException()
 
         assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0))
         assertFalse(backup.hasState())
     }
 
     @Test
-    fun `exception while reading next header`() {
+    fun `exception while reading next header`() = runBlocking {
         initPlugin(false)
         createBackupDataInput()
         every { dataInput.readNextHeader() } throws IOException()
@@ -107,7 +110,7 @@ internal class KVBackupTest : BackupTest() {
     }
 
     @Test
-    fun `exception while reading value`() {
+    fun `exception while reading value`() = runBlocking {
         initPlugin(false)
         createBackupDataInput()
         every { dataInput.readNextHeader() } returns true
@@ -120,7 +123,7 @@ internal class KVBackupTest : BackupTest() {
     }
 
     @Test
-    fun `no data records`() {
+    fun `no data records`() = runBlocking {
         initPlugin(false)
         getDataInput(listOf(false))
 
@@ -131,10 +134,10 @@ internal class KVBackupTest : BackupTest() {
     }
 
     @Test
-    fun `exception while writing version header`() {
+    fun `exception while writing version header`() = runBlocking {
         initPlugin(false)
         getDataInput(listOf(true))
-        every { plugin.getOutputStreamForRecord(packageInfo, key64) } returns outputStream
+        coEvery { plugin.getOutputStreamForRecord(packageInfo, key64) } returns outputStream
         every { headerWriter.writeVersion(outputStream, versionHeader) } throws IOException()
 
         assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0))
@@ -142,11 +145,11 @@ internal class KVBackupTest : BackupTest() {
     }
 
     @Test
-    fun `exception while writing encrypted value to output stream`() {
+    fun `exception while writing encrypted value to output stream`() = runBlocking {
         initPlugin(false)
         getDataInput(listOf(true))
         writeHeaderAndEncrypt()
-        every { plugin.getOutputStreamForRecord(packageInfo, key64) } returns outputStream
+        coEvery { plugin.getOutputStreamForRecord(packageInfo, key64) } returns outputStream
         every { headerWriter.writeVersion(outputStream, versionHeader) } just Runs
         every { crypto.encryptMultipleSegments(outputStream, any()) } throws IOException()
 
@@ -155,7 +158,7 @@ internal class KVBackupTest : BackupTest() {
     }
 
     @Test
-    fun `exception while flushing output stream`() {
+    fun `exception while flushing output stream`() = runBlocking {
         initPlugin(false)
         getDataInput(listOf(true))
         writeHeaderAndEncrypt()
@@ -167,7 +170,7 @@ internal class KVBackupTest : BackupTest() {
     }
 
     @Test
-    fun `ignoring exception while closing output stream`() {
+    fun `ignoring exception while closing output stream`() = runBlocking {
         initPlugin(false)
         getDataInput(listOf(true, false))
         writeHeaderAndEncrypt()
@@ -192,7 +195,7 @@ internal class KVBackupTest : BackupTest() {
 
     private fun initPlugin(hasDataForPackage: Boolean = false) {
         every { plugin.hasDataForPackage(packageInfo) } returns hasDataForPackage
-        every { plugin.ensureRecordStorageForPackage(packageInfo) } just Runs
+        coEvery { plugin.ensureRecordStorageForPackage(packageInfo) } just Runs
     }
 
     private fun createBackupDataInput() {
@@ -208,7 +211,7 @@ internal class KVBackupTest : BackupTest() {
     }
 
     private fun writeHeaderAndEncrypt() {
-        every { plugin.getOutputStreamForRecord(packageInfo, key64) } returns outputStream
+        coEvery { plugin.getOutputStreamForRecord(packageInfo, key64) } returns outputStream
         every { headerWriter.writeVersion(outputStream, versionHeader) } just Runs
         every { crypto.encryptHeader(outputStream, versionHeader) } just Runs
         every { crypto.encryptMultipleSegments(outputStream, any()) } just Runs
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/ApkRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/ApkRestoreTest.kt
index c8a823ab..69ac2074 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/ApkRestoreTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/ApkRestoreTest.kt
@@ -15,6 +15,7 @@ import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.FAILED
 import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.IN_PROGRESS
 import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.QUEUED
 import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.SUCCEEDED
+import io.mockk.coEvery
 import io.mockk.every
 import io.mockk.mockk
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -31,6 +32,7 @@ import java.io.File
 import java.nio.file.Path
 import kotlin.random.Random
 
+@Suppress("BlockingMethodInNonBlockingContext")
 @ExperimentalCoroutinesApi
 internal class ApkRestoreTest : RestoreTest() {
 
@@ -71,7 +73,7 @@ internal class ApkRestoreTest : RestoreTest() {
         val packageMetadataMap: PackageMetadataMap = hashMapOf(packageName to packageMetadata)
 
         every { strictContext.cacheDir } returns File(tmpDir.toString())
-        every { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
+        coEvery { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
 
         apkRestore.restore(token, packageMetadataMap).collectIndexed { index, value ->
             when (index) {
@@ -96,7 +98,7 @@ internal class ApkRestoreTest : RestoreTest() {
         packageInfo.packageName = getRandomString()
 
         every { strictContext.cacheDir } returns File(tmpDir.toString())
-        every { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
+        coEvery { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
         every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
 
         apkRestore.restore(token, packageMetadataMap).collectIndexed { index, value ->
@@ -119,7 +121,7 @@ internal class ApkRestoreTest : RestoreTest() {
     @Test
     fun `test apkInstaller throws exceptions`(@TempDir tmpDir: Path) = runBlocking {
         every { strictContext.cacheDir } returns File(tmpDir.toString())
-        every { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
+        coEvery { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
         every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
         every { pm.loadItemIcon(packageInfo.applicationInfo, packageInfo.applicationInfo) } returns icon
         every { pm.getApplicationLabel(packageInfo.applicationInfo) } returns appName
@@ -155,7 +157,7 @@ internal class ApkRestoreTest : RestoreTest() {
         }
 
         every { strictContext.cacheDir } returns File(tmpDir.toString())
-        every { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
+        coEvery { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
         every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
         every { pm.loadItemIcon(packageInfo.applicationInfo, packageInfo.applicationInfo) } returns icon
         every { pm.getApplicationLabel(packageInfo.applicationInfo) } returns appName
@@ -199,7 +201,7 @@ internal class ApkRestoreTest : RestoreTest() {
         }
 
         every { strictContext.cacheDir } returns File(tmpDir.toString())
-        every { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
+        coEvery { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
         every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
         every { pm.loadItemIcon(packageInfo.applicationInfo, packageInfo.applicationInfo) } returns icon
         every { packageInfo.applicationInfo.loadIcon(pm) } returns icon
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt
index 46f5532e..8fd4ee27 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt
@@ -4,18 +4,20 @@ import android.app.backup.BackupTransport.NO_MORE_DATA
 import android.app.backup.BackupTransport.TRANSPORT_ERROR
 import android.app.backup.BackupTransport.TRANSPORT_OK
 import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
+import com.stevesoltys.seedvault.coAssertThrows
 import com.stevesoltys.seedvault.getRandomByteArray
 import com.stevesoltys.seedvault.header.UnsupportedVersionException
 import com.stevesoltys.seedvault.header.VERSION
 import com.stevesoltys.seedvault.header.VersionHeader
 import io.mockk.Runs
+import io.mockk.coEvery
 import io.mockk.every
 import io.mockk.just
 import io.mockk.mockk
+import kotlinx.coroutines.runBlocking
 import org.junit.jupiter.api.Assertions.assertArrayEquals
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Assertions.assertFalse
-import org.junit.jupiter.api.Assertions.assertThrows
 import org.junit.jupiter.api.Assertions.assertTrue
 import org.junit.jupiter.api.Test
 import java.io.ByteArrayOutputStream
@@ -23,6 +25,7 @@ import java.io.EOFException
 import java.io.IOException
 import kotlin.random.Random
 
+@Suppress("BlockingMethodInNonBlockingContext")
 internal class FullRestoreTest : RestoreTest() {
 
     private val plugin = mockk<FullRestorePlugin>()
@@ -38,9 +41,9 @@ internal class FullRestoreTest : RestoreTest() {
     }
 
     @Test
-    fun `hasDataForPackage() delegates to plugin`() {
+    fun `hasDataForPackage() delegates to plugin`() = runBlocking {
         val result = Random.nextBoolean()
-        every { plugin.hasDataForPackage(token, packageInfo) } returns result
+        coEvery { plugin.hasDataForPackage(token, packageInfo) } returns result
         assertEquals(result, restore.hasDataForPackage(token, packageInfo))
     }
 
@@ -54,45 +57,45 @@ internal class FullRestoreTest : RestoreTest() {
     @Test
     fun `getting chunks without initializing state throws`() {
         assertFalse(restore.hasState())
-        assertThrows(IllegalStateException::class.java) {
+        coAssertThrows(IllegalStateException::class.java) {
             restore.getNextFullRestoreDataChunk(fileDescriptor)
         }
     }
 
     @Test
-    fun `getting InputStream for package when getting first chunk throws`() {
+    fun `getting InputStream for package when getting first chunk throws`() = runBlocking {
         restore.initializeState(token, packageInfo)
 
-        every { plugin.getInputStreamForPackage(token, packageInfo) } throws IOException()
+        coEvery { plugin.getInputStreamForPackage(token, packageInfo) } throws IOException()
 
         assertEquals(TRANSPORT_PACKAGE_REJECTED, restore.getNextFullRestoreDataChunk(fileDescriptor))
     }
 
     @Test
-    fun `reading version header when getting first chunk throws`() {
+    fun `reading version header when getting first chunk throws`() = runBlocking {
         restore.initializeState(token, packageInfo)
 
-        every { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
+        coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
         every { headerReader.readVersion(inputStream) } throws IOException()
 
         assertEquals(TRANSPORT_PACKAGE_REJECTED, restore.getNextFullRestoreDataChunk(fileDescriptor))
     }
 
     @Test
-    fun `reading unsupported version when getting first chunk`() {
+    fun `reading unsupported version when getting first chunk`() = runBlocking {
         restore.initializeState(token, packageInfo)
 
-        every { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
+        coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
         every { headerReader.readVersion(inputStream) } throws UnsupportedVersionException(unsupportedVersion)
 
         assertEquals(TRANSPORT_PACKAGE_REJECTED, restore.getNextFullRestoreDataChunk(fileDescriptor))
     }
 
     @Test
-    fun `decrypting version header when getting first chunk throws`() {
+    fun `decrypting version header when getting first chunk throws`() = runBlocking {
         restore.initializeState(token, packageInfo)
 
-        every { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
+        coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
         every { headerReader.readVersion(inputStream) } returns VERSION
         every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName) } throws IOException()
 
@@ -100,10 +103,10 @@ internal class FullRestoreTest : RestoreTest() {
     }
 
     @Test
-    fun `decrypting version header when getting first chunk throws security exception`() {
+    fun `decrypting version header when getting first chunk throws security exception`() = runBlocking {
         restore.initializeState(token, packageInfo)
 
-        every { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
+        coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
         every { headerReader.readVersion(inputStream) } returns VERSION
         every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName) } throws SecurityException()
 
@@ -111,7 +114,7 @@ internal class FullRestoreTest : RestoreTest() {
     }
 
     @Test
-    fun `decrypting segment throws IOException`() {
+    fun `decrypting segment throws IOException`() = runBlocking {
         restore.initializeState(token, packageInfo)
 
         initInputStream()
@@ -124,7 +127,7 @@ internal class FullRestoreTest : RestoreTest() {
     }
 
     @Test
-    fun `decrypting segment throws EOFException`() {
+    fun `decrypting segment throws EOFException`() = runBlocking {
         restore.initializeState(token, packageInfo)
 
         initInputStream()
@@ -137,7 +140,7 @@ internal class FullRestoreTest : RestoreTest() {
     }
 
     @Test
-    fun `full chunk gets encrypted`() {
+    fun `full chunk gets encrypted`() = runBlocking {
         restore.initializeState(token, packageInfo)
 
         initInputStream()
@@ -151,7 +154,7 @@ internal class FullRestoreTest : RestoreTest() {
     }
 
     @Test
-    fun `aborting full restore closes stream, resets state`() {
+    fun `aborting full restore closes stream, resets state`() = runBlocking {
         restore.initializeState(token, packageInfo)
 
         initInputStream()
@@ -166,7 +169,7 @@ internal class FullRestoreTest : RestoreTest() {
     }
 
     private fun initInputStream() {
-        every { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
+        coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
         every { headerReader.readVersion(inputStream) } returns VERSION
         every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName) } returns versionHeader
     }
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 db0c29a3..e9692cf8 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
@@ -9,10 +9,12 @@ import com.stevesoltys.seedvault.header.UnsupportedVersionException
 import com.stevesoltys.seedvault.header.VERSION
 import com.stevesoltys.seedvault.header.VersionHeader
 import io.mockk.Runs
+import io.mockk.coEvery
 import io.mockk.every
 import io.mockk.just
 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
@@ -20,6 +22,7 @@ import java.io.IOException
 import java.io.InputStream
 import kotlin.random.Random
 
+@Suppress("BlockingMethodInNonBlockingContext")
 internal class KVRestoreTest : RestoreTest() {
 
     private val plugin = mockk<KVRestorePlugin>()
@@ -34,10 +37,10 @@ internal class KVRestoreTest : RestoreTest() {
     private val versionHeader2 = VersionHeader(VERSION, packageInfo.packageName, key2)
 
     @Test
-    fun `hasDataForPackage() delegates to plugin`() {
+    fun `hasDataForPackage() delegates to plugin`() = runBlocking {
         val result = Random.nextBoolean()
 
-        every { plugin.hasDataForPackage(token, packageInfo) } returns result
+        coEvery { plugin.hasDataForPackage(token, packageInfo) } returns result
 
         assertEquals(result, restore.hasDataForPackage(token, packageInfo))
     }
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 fd8b5ca5..05cf90b3 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
@@ -10,6 +10,7 @@ import android.content.pm.PackageInfo
 import android.os.ParcelFileDescriptor
 import androidx.documentfile.provider.DocumentFile
 import com.stevesoltys.seedvault.BackupNotificationManager
+import com.stevesoltys.seedvault.coAssertThrows
 import com.stevesoltys.seedvault.getRandomString
 import com.stevesoltys.seedvault.metadata.BackupMetadata
 import com.stevesoltys.seedvault.metadata.EncryptedBackupMetadata
@@ -18,10 +19,12 @@ import com.stevesoltys.seedvault.metadata.PackageMetadata
 import com.stevesoltys.seedvault.settings.Storage
 import com.stevesoltys.seedvault.transport.TransportTest
 import io.mockk.Runs
+import io.mockk.coEvery
 import io.mockk.every
 import io.mockk.just
 import io.mockk.mockk
 import io.mockk.verify
+import kotlinx.coroutines.runBlocking
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Assertions.assertNotNull
 import org.junit.jupiter.api.Assertions.assertNull
@@ -32,6 +35,7 @@ import java.io.IOException
 import java.io.InputStream
 import kotlin.random.Random
 
+@Suppress("BlockingMethodInNonBlockingContext")
 internal class RestoreCoordinatorTest : TransportTest() {
 
     private val notificationManager: BackupNotificationManager = mockk()
@@ -57,7 +61,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
     private val storageName = getRandomString()
 
     @Test
-    fun `getAvailableRestoreSets() builds set from plugin response`() {
+    fun `getAvailableRestoreSets() builds set from plugin response`() = runBlocking {
         val encryptedMetadata = EncryptedBackupMetadata(token, inputStream)
         val metadata = BackupMetadata(
                 token = token,
@@ -65,7 +69,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
                 androidIncremental = getRandomString(),
                 deviceName = getRandomString())
 
-        every { plugin.getAvailableBackups() } returns sequenceOf(encryptedMetadata, encryptedMetadata)
+        coEvery { plugin.getAvailableBackups() } returns sequenceOf(encryptedMetadata, encryptedMetadata)
         every { metadataReader.readMetadata(inputStream, token) } returns metadata
         every { inputStream.close() } just Runs
 
@@ -137,16 +141,16 @@ internal class RestoreCoordinatorTest : TransportTest() {
 
     @Test
     fun `nextRestorePackage() throws without startRestore()`() {
-        assertThrows(IllegalStateException::class.javaObjectType) {
+        coAssertThrows(IllegalStateException::class.javaObjectType) {
             restore.nextRestorePackage()
         }
     }
 
     @Test
-    fun `nextRestorePackage() returns KV description and takes precedence`() {
+    fun `nextRestorePackage() returns KV description and takes precedence`() = runBlocking {
         restore.startRestore(token, packageInfoArray)
 
-        every { kv.hasDataForPackage(token, packageInfo) } returns true
+        coEvery { kv.hasDataForPackage(token, packageInfo) } returns true
         every { kv.initializeState(token, packageInfo) } just Runs
 
         val expected = RestoreDescription(packageInfo.packageName, TYPE_KEY_VALUE)
@@ -154,11 +158,11 @@ internal class RestoreCoordinatorTest : TransportTest() {
     }
 
     @Test
-    fun `nextRestorePackage() returns full description if no KV data found`() {
+    fun `nextRestorePackage() returns full description if no KV data found`() = runBlocking {
         restore.startRestore(token, packageInfoArray)
 
-        every { kv.hasDataForPackage(token, packageInfo) } returns false
-        every { full.hasDataForPackage(token, packageInfo) } returns true
+        coEvery { kv.hasDataForPackage(token, packageInfo) } returns false
+        coEvery { full.hasDataForPackage(token, packageInfo) } returns true
         every { full.initializeState(token, packageInfo) } just Runs
 
         val expected = RestoreDescription(packageInfo.packageName, TYPE_FULL_STREAM)
@@ -166,27 +170,27 @@ internal class RestoreCoordinatorTest : TransportTest() {
     }
 
     @Test
-    fun `nextRestorePackage() returns NO_MORE_PACKAGES if data found`() {
+    fun `nextRestorePackage() returns NO_MORE_PACKAGES if data found`() = runBlocking {
         restore.startRestore(token, packageInfoArray)
 
-        every { kv.hasDataForPackage(token, packageInfo) } returns false
-        every { full.hasDataForPackage(token, packageInfo) } returns false
+        coEvery { kv.hasDataForPackage(token, packageInfo) } returns false
+        coEvery { full.hasDataForPackage(token, packageInfo) } returns false
 
         assertEquals(NO_MORE_PACKAGES, restore.nextRestorePackage())
     }
 
     @Test
-    fun `nextRestorePackage() returns all packages from startRestore()`() {
+    fun `nextRestorePackage() returns all packages from startRestore()`() = runBlocking {
         restore.startRestore(token, packageInfoArray2)
 
-        every { kv.hasDataForPackage(token, packageInfo) } returns true
+        coEvery { kv.hasDataForPackage(token, packageInfo) } returns true
         every { kv.initializeState(token, packageInfo) } just Runs
 
         val expected = RestoreDescription(packageInfo.packageName, TYPE_KEY_VALUE)
         assertEquals(expected, restore.nextRestorePackage())
 
-        every { kv.hasDataForPackage(token, packageInfo2) } returns false
-        every { full.hasDataForPackage(token, packageInfo2) } returns true
+        coEvery { kv.hasDataForPackage(token, packageInfo2) } returns false
+        coEvery { full.hasDataForPackage(token, packageInfo2) } returns true
         every { full.initializeState(token, packageInfo2) } just Runs
 
         val expected2 = RestoreDescription(packageInfo2.packageName, TYPE_FULL_STREAM)
@@ -196,20 +200,20 @@ internal class RestoreCoordinatorTest : TransportTest() {
     }
 
     @Test
-    fun `when kv#hasDataForPackage() throws return null`() {
+    fun `when kv#hasDataForPackage() throws return null`() = runBlocking {
         restore.startRestore(token, packageInfoArray)
 
-        every { kv.hasDataForPackage(token, packageInfo) } throws IOException()
+        coEvery { kv.hasDataForPackage(token, packageInfo) } throws IOException()
 
         assertNull(restore.nextRestorePackage())
     }
 
     @Test
-    fun `when full#hasDataForPackage() throws return null`() {
+    fun `when full#hasDataForPackage() throws return null`() = runBlocking {
         restore.startRestore(token, packageInfoArray)
 
-        every { kv.hasDataForPackage(token, packageInfo) } returns false
-        every { full.hasDataForPackage(token, packageInfo) } throws IOException()
+        coEvery { kv.hasDataForPackage(token, packageInfo) } returns false
+        coEvery { full.hasDataForPackage(token, packageInfo) } throws IOException()
 
         assertNull(restore.nextRestorePackage())
     }
@@ -225,11 +229,11 @@ internal class RestoreCoordinatorTest : TransportTest() {
     }
 
     @Test
-    fun `getNextFullRestoreDataChunk() delegates to Full`() {
+    fun `getNextFullRestoreDataChunk() delegates to Full`() = runBlocking {
         val data = mockk<ParcelFileDescriptor>()
         val result = Random.nextInt()
 
-        every { full.getNextFullRestoreDataChunk(data) } returns result
+        coEvery { full.getNextFullRestoreDataChunk(data) } returns result
 
         assertEquals(result, restore.getNextFullRestoreDataChunk(data))
     }