From e9b79d88b93779599a42a4b5b80515ec304ca730 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 6 Aug 2020 12:33:36 -0300 Subject: [PATCH 01/20] Upgrade gradle --- app/build.gradle | 5 ++++- build.gradle | 2 ++ gradle.properties | 3 +++ gradle/wrapper/gradle-wrapper.properties | 6 +++--- 4 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 gradle.properties diff --git a/app/build.gradle b/app/build.gradle index de921d5b..afcf4356 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -129,14 +129,17 @@ dependencies { lintChecks 'com.github.thirdegg:lint-rules:0.0.4-alpha' def junit_version = "5.5.2" + def mockk_version = "1.10.0" testImplementation aospDeps testImplementation 'androidx.test.ext:junit:1.1.1' testImplementation 'org.robolectric:robolectric:4.3.1' testImplementation "org.junit.jupiter:junit-jupiter-api:$junit_version" - testImplementation 'io.mockk:mockk:1.9.3' + testImplementation "io.mockk:mockk:$mockk_version" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit_version" testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$junit_version" androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation "io.mockk:mockk-android:$mockk_version" } diff --git a/build.gradle b/build.gradle index 65385e66..449e85d4 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,8 @@ buildscript { + // 1.3.21 Android 10 + // 1.3.72 AOSP master (2020-08) ext.kotlin_version = '1.3.61' repositories { diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..f3edb44c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1g +android.useAndroidX=true +android.enableJetifier=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 39c9f096..9ff0d9fc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ -#Thu Nov 08 02:00:38 GMT 2018 +#Tue Aug 04 14:40:48 BRT 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip -distributionSha256Sum=14cd15fc8cc8705bd69dcfa3c8fefb27eb7027f5de4b47a8b279218f76895a91 \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip +distributionSha256Sum=143a28f54f1ae93ef4f72d862dbc3c438050d81bb45b4601eb7076e998362920 From 523901a0a97b5c543e732f0a8972418f15402f71 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 6 Aug 2020 13:49:33 -0300 Subject: [PATCH 02/20] Add instrumentation tests for storage plugin (SAF) --- .../seedvault/DocumentsStorageTest.kt | 2 +- .../com/stevesoltys/seedvault/PluginTest.kt | 252 ++++++++++++++++++ .../saf/DocumentsProviderFullRestorePlugin.kt | 2 +- .../saf/DocumentsProviderRestorePlugin.kt | 2 +- .../seedvault/plugins/saf/DocumentsStorage.kt | 1 + .../seedvault/settings/SettingsManager.kt | 1 + .../transport/restore/KVRestorePlugin.kt | 4 + .../com/stevesoltys/seedvault/TestUtils.kt | 12 +- 8 files changed, 272 insertions(+), 4 deletions(-) create mode 100644 app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/DocumentsStorageTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/DocumentsStorageTest.kt index 3b673dbb..6a9cba82 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/DocumentsStorageTest.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/DocumentsStorageTest.kt @@ -2,7 +2,7 @@ package com.stevesoltys.seedvault import androidx.documentfile.provider.DocumentFile import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.runner.AndroidJUnit4 +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 diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt new file mode 100644 index 00000000..7f9954df --- /dev/null +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt @@ -0,0 +1,252 @@ +package com.stevesoltys.seedvault + +import androidx.test.core.content.pm.PackageInfoBuilder +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.stevesoltys.seedvault.metadata.MetadataManager +import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderBackupPlugin +import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderRestorePlugin +import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage +import com.stevesoltys.seedvault.plugins.saf.deleteContents +import com.stevesoltys.seedvault.settings.SettingsManager +import com.stevesoltys.seedvault.transport.backup.BackupPlugin +import com.stevesoltys.seedvault.transport.restore.RestorePlugin +import io.mockk.every +import io.mockk.mockk +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 +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) +class PluginTest : KoinComponent { + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val metadataManager: MetadataManager by inject() + 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 restorePlugin: RestorePlugin = DocumentsProviderRestorePlugin(context, storage) + + private val token = Random.nextLong() + private val packageInfo = PackageInfoBuilder.newBuilder().setPackageName("org.example").build() + private val packageInfo2 = PackageInfoBuilder.newBuilder().setPackageName("net.example").build() + + @Before + fun setup() { + every { mockedSettingsManager.getStorage() } returns settingsManager.getStorage() + storage.rootBackupDir?.deleteContents() + ?: error("Select a storage location in the app first!") + } + + @After + fun tearDown() { + storage.rootBackupDir?.deleteContents() + } + + @Test + fun testProviderPackageName() { + assertNotNull(backupPlugin.providerPackageName) + } + + @Test + fun testMetadataWriteRead() { + every { mockedSettingsManager.getAndResetIsStorageChanging() } returns true andThen false + assertTrue(backupPlugin.initializeDevice(newToken = token)) + + // write metadata + val metadata = getRandomByteArray() + backupPlugin.getMetadataOutputStream().writeAndClose(metadata) + + // get available backups, expect only one with our token and no error + var availableBackups = restorePlugin.getAvailableBackups()?.toList() + check(availableBackups != null) + assertEquals(1, availableBackups.size) + assertEquals(token, availableBackups[0].token) + assertFalse(availableBackups[0].error) + + // read metadata matches what was written earlier + assertEquals(metadata, availableBackups[0].inputStream) + + // initializing again (without changing storage) keeps restore set with same token + assertFalse(backupPlugin.initializeDevice(newToken = token + 1)) + availableBackups = restorePlugin.getAvailableBackups()?.toList() + check(availableBackups != null) + assertEquals(1, availableBackups.size) + assertEquals(token, availableBackups[0].token) + assertFalse(availableBackups[0].error) + + // metadata hasn't changed + assertEquals(metadata, availableBackups[0].inputStream) + } + + @Test + fun testApkWriteRead() { + // initialize storage with given token + initStorage(token) + + // write random bytes as APK + val apk = getRandomByteArray(1337) + backupPlugin.getApkOutputStream(packageInfo).writeAndClose(apk) + + // assert that read APK bytes match what was written + assertEquals(apk, restorePlugin.getApkInputStream(token, packageInfo.packageName)) + } + + @Test + fun testKvBackupRestore() { + // define shortcuts + val kvBackup = backupPlugin.kvBackupPlugin + val kvRestore = restorePlugin.kvRestorePlugin + + // initialize storage with given token + initStorage(token) + + // no data available for given package + assertFalse(kvBackup.hasDataForPackage(packageInfo)) + assertFalse(kvRestore.hasDataForPackage(token, packageInfo)) + + // define key/value pair records + val record1 = Pair(getRandomBase64(23), getRandomByteArray(1337)) + val record2 = Pair(getRandomBase64(42), getRandomByteArray(42 * 1024)) + val record3 = Pair(getRandomBase64(255), getRandomByteArray(5 * 1024 * 1024)) + + // write first record + kvBackup.ensureRecordStorageForPackage(packageInfo) + kvBackup.getOutputStreamForRecord(packageInfo, record1.first).writeAndClose(record1.second) + + // data is now available for current token and given package, but not for different token + assertTrue(kvBackup.hasDataForPackage(packageInfo)) + assertTrue(kvRestore.hasDataForPackage(token, packageInfo)) + assertFalse(kvRestore.hasDataForPackage(token + 1, packageInfo)) + + // record for package is found and returned properly + var records = kvRestore.listRecords(token, packageInfo) + assertEquals(1, records.size) + assertEquals(record1.first, records[0]) + assertEquals(record1.second, kvRestore.getInputStreamForRecord(token, packageInfo, record1.first)) + + // write second and third record + kvBackup.ensureRecordStorageForPackage(packageInfo) + kvBackup.getOutputStreamForRecord(packageInfo, record2.first).writeAndClose(record2.second) + kvBackup.getOutputStreamForRecord(packageInfo, record3.first).writeAndClose(record3.second) + + // 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)) + + // delete record3 and ensure that the other two are still found + kvBackup.deleteRecord(packageInfo, record3.first) + records = kvRestore.listRecords(token, packageInfo) + assertEquals(listOf(record1.first, record2.first).sorted(), records.sorted()) + + // remove all data of package and ensure that it is gone + kvBackup.removeDataOfPackage(packageInfo) + assertFalse(kvBackup.hasDataForPackage(packageInfo)) + assertFalse(kvRestore.hasDataForPackage(token, packageInfo)) + } + + @Test + fun testMaxKvKeyLength() { + // define shortcuts + val kvBackup = backupPlugin.kvBackupPlugin + val kvRestore = restorePlugin.kvRestorePlugin + + // initialize storage with given token + initStorage(token) + + // define record with maximum key length and one above the maximum + val recordMax = Pair(getRandomBase64(255), getRandomByteArray(1024)) + val recordOver = Pair(getRandomBase64(256), getRandomByteArray(1024)) + + // write max record + kvBackup.ensureRecordStorageForPackage(packageInfo) + kvBackup.getOutputStreamForRecord(packageInfo, recordMax.first).writeAndClose(recordMax.second) + + // max record is found correctly + assertTrue(kvRestore.hasDataForPackage(token, packageInfo)) + var 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()) + } + + @Test + fun testFullBackupRestore() { + // define shortcuts + val fullBackup = backupPlugin.fullBackupPlugin + val fullRestore = restorePlugin.fullRestorePlugin + + // initialize storage with given token + initStorage(token) + + // no data available initially + assertFalse(fullRestore.hasDataForPackage(token, packageInfo)) + assertFalse(fullRestore.hasDataForPackage(token, packageInfo2)) + + // write full backup data + val data = getRandomByteArray(5 * 1024 * 1024) + fullBackup.getOutputStream(packageInfo).writeAndClose(data) + + // data is available now, but only this token + assertTrue(fullRestore.hasDataForPackage(token, packageInfo)) + assertFalse(fullRestore.hasDataForPackage(token + 1, packageInfo)) + + // restore data matches backed up data + assertEquals(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)) + + // remove data of first package again and ensure that no more data is found + fullBackup.removeDataOfPackage(packageInfo) + assertFalse(fullRestore.hasDataForPackage(token, packageInfo)) + + // second package is still there + assertTrue(fullRestore.hasDataForPackage(token, packageInfo2)) + + // ensure that it gets deleted as well + fullBackup.removeDataOfPackage(packageInfo2) + assertFalse(fullRestore.hasDataForPackage(token, packageInfo2)) + } + + private fun initStorage(token: Long) { + 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/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderFullRestorePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderFullRestorePlugin.kt index 3ed21176..57a0ae18 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 @@ -10,7 +10,7 @@ internal class DocumentsProviderFullRestorePlugin( @Throws(IOException::class) override fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean { - val backupDir = documentsStorage.getFullBackupDir(token) ?: throw IOException() + val backupDir = documentsStorage.getFullBackupDir(token) ?: return false return backupDir.findFile(packageInfo.packageName) != null } 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 d9375c2a..142046ab 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 @@ -53,7 +53,7 @@ internal class DocumentsProviderRestorePlugin( } @WorkerThread - fun getBackups(context: Context, rootDir: DocumentFile): List { + private fun getBackups(context: Context, rootDir: DocumentFile): List { val backupSets = ArrayList() val files = try { // block until the DocumentsProvider has results 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 ff91fa9d..702aa429 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 @@ -204,6 +204,7 @@ fun DocumentFile.listFilesBlocking(context: Context): ArrayList { }) 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 diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt index 949bc2d9..913e3a21 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -49,6 +49,7 @@ class SettingsManager(context: Context) { return Storage(uri, name, isUsb) } + // TODO find a better solution for this hack abusing the settings manager fun getAndResetIsStorageChanging(): Boolean { return isStorageChanging.getAndSet(false) } 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 84b9d9e0..4769e3e5 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 @@ -15,6 +15,8 @@ interface KVRestorePlugin { /** * Return all record keys for the given token and package. * + * Note: Implementations might expect that you call [hasDataForPackage] before. + * * For file-based plugins, this is usually a list of file names in the package directory. */ @Throws(IOException::class) @@ -23,6 +25,8 @@ interface KVRestorePlugin { /** * Return an [InputStream] for the given token, package and key * which will provide the record's encrypted value. + * + * Note: Implementations might expect that you call [hasDataForPackage] before. */ @Throws(IOException::class) fun getInputStreamForRecord(token: Long, packageInfo: PackageInfo, key: String): InputStream diff --git a/app/src/sharedTest/java/com/stevesoltys/seedvault/TestUtils.kt b/app/src/sharedTest/java/com/stevesoltys/seedvault/TestUtils.kt index 517daf37..bc0e211b 100644 --- a/app/src/sharedTest/java/com/stevesoltys/seedvault/TestUtils.kt +++ b/app/src/sharedTest/java/com/stevesoltys/seedvault/TestUtils.kt @@ -10,7 +10,7 @@ fun getRandomByteArray(size: Int = Random.nextInt(1337)) = ByteArray(size).apply Random.nextBytes(this) } -private val charPool : List = ('a'..'z') + ('A'..'Z') + ('0'..'9') + '_' + '.' +private val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') + '_' + '.' fun getRandomString(size: Int = Random.nextInt(1, 255)): String { return (1..size) @@ -19,6 +19,16 @@ fun getRandomString(size: Int = Random.nextInt(1, 255)): String { .joinToString("") } +// URL-save version (RFC 4648) +private val base64CharPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') + '+' + '_' + '=' + +fun getRandomBase64(size: Int = Random.nextInt(1, 255)): String { + return (1..size) + .map { Random.nextInt(0, base64CharPool.size) } + .map(base64CharPool::get) + .joinToString("") +} + fun ByteArray.toHexString(): String { var str = "" for (b in this) { From 1913621fd3dd5b6e84d26cf55624f1df31848e0c Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 6 Aug 2020 16:43:15 -0300 Subject: [PATCH 03/20] Add test to reproduce the loading cursor phenomena with Nextcloud See: https://commonsware.com/blog/2019/12/14/scoped-storage-stories-listfiles-woe.html --- .../com/stevesoltys/seedvault/PluginTest.kt | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt index 7f9954df..d2251636 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt @@ -62,6 +62,47 @@ class PluginTest : KoinComponent { assertNotNull(backupPlugin.providerPackageName) } + /** + * This test initializes the storage three times while creating two new restore sets. + * + * If this is run against a Nextcloud storage backend, + * it has a high chance of getting a loading cursor in the underlying queries + * that needs to get re-queried to get real results. + */ + @Test + fun testInitializationAndRestoreSets() { + // no backups available initially + assertEquals(0, restorePlugin.getAvailableBackups()?.toList()?.size) + val uri = settingsManager.getStorage()?.getDocumentFile(context)?.uri ?: error("no storage") + assertFalse(restorePlugin.hasBackup(uri)) + + // define storage changing state for later + every { + mockedSettingsManager.getAndResetIsStorageChanging() + } returns true andThen true andThen false + + // device needs initialization, because new and storage is changing + assertTrue(backupPlugin.initializeDevice(newToken = token)) + + // write metadata (needed for backup to be recognized) + backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray()) + + // one backup available now + assertEquals(1, restorePlugin.getAvailableBackups()?.toList()?.size) + assertTrue(restorePlugin.hasBackup(uri)) + + // initializing again (while changing storage) does add a restore set + assertTrue(backupPlugin.initializeDevice(newToken = token + 1)) + backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray()) + assertEquals(2, restorePlugin.getAvailableBackups()?.toList()?.size) + assertTrue(restorePlugin.hasBackup(uri)) + + // initializing again (without changing storage) doesn't change number of restore sets + assertFalse(backupPlugin.initializeDevice(newToken = token + 2)) + backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray()) + assertEquals(2, restorePlugin.getAvailableBackups()?.toList()?.size) + } + @Test fun testMetadataWriteRead() { every { mockedSettingsManager.getAndResetIsStorageChanging() } returns true andThen false From b360a8ff1c17bc8ee62592abc56bc832f4ede2dc Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 7 Aug 2020 16:57:27 -0300 Subject: [PATCH 04/20] Check for loading cursor also when checking if files exist Loading cursors can happen with cloud-based documents providers such as Nextcloud. When they return a cursor that is still loading, we might continue with stale information. So now we wait for a loading cursor to be fully loaded before continuing. --- .gitignore | 3 +- .idea/runConfigurations.xml | 12 ++ .idea/runConfigurations/Unit_Tests.xml | 17 ++ app/build.gradle | 6 +- .../seedvault/DocumentsStorageTest.kt | 73 ------- .../com/stevesoltys/seedvault/PluginTest.kt | 60 +++--- .../plugins/saf/DocumentsStorageTest.kt | 174 ++++++++++++++++ .../saf/DocumentsProviderBackupPlugin.kt | 21 +- .../saf/DocumentsProviderFullBackup.kt | 8 +- .../saf/DocumentsProviderFullRestorePlugin.kt | 4 +- .../plugins/saf/DocumentsProviderKVBackup.kt | 14 +- .../saf/DocumentsProviderKVRestorePlugin.kt | 2 +- .../plugins/saf/DocumentsProviderModule.kt | 2 +- .../saf/DocumentsProviderRestorePlugin.kt | 20 +- .../seedvault/plugins/saf/DocumentsStorage.kt | 197 +++++++++++------- .../seedvault/restore/RestoreViewModel.kt | 3 +- .../transport/ConfigurableBackupTransport.kt | 43 ++-- .../seedvault/transport/backup/ApkBackup.kt | 2 +- .../transport/backup/BackupCoordinator.kt | 25 ++- .../transport/backup/BackupPlugin.kt | 6 +- .../seedvault/transport/backup/FullBackup.kt | 13 +- .../transport/backup/FullBackupPlugin.kt | 2 +- .../seedvault/transport/backup/KVBackup.kt | 5 +- .../transport/backup/KVBackupPlugin.kt | 4 +- .../transport/restore/FullRestore.kt | 6 +- .../transport/restore/FullRestorePlugin.kt | 4 +- .../seedvault/transport/restore/KVRestore.kt | 2 +- .../transport/restore/KVRestorePlugin.kt | 2 +- .../transport/restore/RestoreCoordinator.kt | 7 +- .../transport/restore/RestorePlugin.kt | 7 +- .../ui/storage/RestoreStorageViewModel.kt | 32 ++- .../com/stevesoltys/seedvault/TestUtils.kt | 27 +++ .../transport/CoordinatorIntegrationTest.kt | 43 ++-- .../transport/backup/ApkBackupTest.kt | 23 +- .../transport/backup/BackupCoordinatorTest.kt | 70 ++++--- .../transport/backup/FullBackupTest.kt | 37 ++-- .../transport/backup/KVBackupTest.kt | 39 ++-- .../transport/restore/ApkRestoreTest.kt | 12 +- .../transport/restore/FullRestoreTest.kt | 41 ++-- .../transport/restore/KVRestoreTest.kt | 7 +- .../restore/RestoreCoordinatorTest.kt | 48 +++-- 41 files changed, 695 insertions(+), 428 deletions(-) create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/runConfigurations/Unit_Tests.xml delete mode 100644 app/src/androidTest/java/com/stevesoltys/seedvault/DocumentsStorageTest.kt create mode 100644 app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt 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 @@ + + + + +