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 @@
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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()
- private val settingsManager by inject()
- 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()
+ private val settingsManager by inject()
+ 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()
+ 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()
+ 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 { DocumentsProviderBackupPlugin(get(), androidContext().packageManager) }
+ single { DocumentsProviderBackupPlugin(androidContext(), get()) }
single { 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? {
+ override suspend fun getAvailableBackups(): Sequence? {
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 {
+ private suspend fun getBackups(context: Context, rootDir: DocumentFile): List {
val backupSets = ArrayList()
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 {
+suspend fun DocumentFile.listFilesBlocking(context: Context): ArrayList {
val resolver = context.contentResolver
val childrenUri = buildChildDocumentsUriUsingTree(uri, getDocumentId(uri))
val projection = arrayOf(COLUMN_DOCUMENT_ID, COLUMN_MIME_TYPE)
val result = ArrayList()
- @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 {
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 { 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? {
- return restoreCoordinator.getAvailableRestoreSets()
+ override fun getAvailableRestoreSets(): Array? = 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? {
+ suspend fun getAvailableRestoreSets(): Array? {
val availableBackups = plugin.getAvailableBackups() ?: return null
val restoreSets = ArrayList()
val metadataMap = LongSparseArray()
@@ -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?
+ suspend fun getAvailableBackups(): Sequence?
/**
* 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 coAssertThrows(clazz: Class, 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()
@@ -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()
val value2 = CapturingSlot()
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()
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()
@@ -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()
val documentFile = mockk()
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()
@@ -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()
@@ -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()
@@ -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()
@@ -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()
val result = Random.nextInt()
- every { full.getNextFullRestoreDataChunk(data) } returns result
+ coEvery { full.getNextFullRestoreDataChunk(data) } returns result
assertEquals(result, restore.getNextFullRestoreDataChunk(data))
}