diff --git a/.idea/runConfigurations/Instrumentation_Tests.xml b/.idea/runConfigurations/Instrumentation_Tests.xml
new file mode 100644
index 00000000..0546643d
--- /dev/null
+++ b/.idea/runConfigurations/Instrumentation_Tests.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/CipherUniqueNonceTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/CipherUniqueNonceTest.kt
index e902b8e6..66687348 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/CipherUniqueNonceTest.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/CipherUniqueNonceTest.kt
@@ -1,8 +1,8 @@
package com.stevesoltys.seedvault
import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
-import androidx.test.runner.AndroidJUnit4
import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
import org.junit.Assert.assertTrue
diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
index 19fdc66c..7bbc04e9 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt
@@ -3,8 +3,11 @@ 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.DocumentsProviderFullBackup
+import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderFullRestorePlugin
+import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderKVBackup
+import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderKVRestorePlugin
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderRestorePlugin
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
import com.stevesoltys.seedvault.plugins.saf.MAX_KEY_LENGTH
@@ -12,6 +15,10 @@ import com.stevesoltys.seedvault.plugins.saf.MAX_KEY_LENGTH_NEXTCLOUD
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.backup.FullBackupPlugin
+import com.stevesoltys.seedvault.transport.backup.KVBackupPlugin
+import com.stevesoltys.seedvault.transport.restore.FullRestorePlugin
+import com.stevesoltys.seedvault.transport.restore.KVRestorePlugin
import com.stevesoltys.seedvault.transport.restore.RestorePlugin
import io.mockk.every
import io.mockk.mockk
@@ -35,27 +42,41 @@ import kotlin.random.Random
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(context, storage)
- private val restorePlugin: RestorePlugin = DocumentsProviderRestorePlugin(context, storage)
+ private val storage = DocumentsStorage(context, mockedSettingsManager)
+
+ private val kvBackupPlugin: KVBackupPlugin = DocumentsProviderKVBackup(context, storage)
+ private val fullBackupPlugin: FullBackupPlugin = DocumentsProviderFullBackup(context, storage)
+ private val backupPlugin: BackupPlugin = DocumentsProviderBackupPlugin(
+ context,
+ storage,
+ kvBackupPlugin,
+ fullBackupPlugin
+ )
+
+ private val kvRestorePlugin: KVRestorePlugin =
+ DocumentsProviderKVRestorePlugin(context, storage)
+ private val fullRestorePlugin: FullRestorePlugin =
+ DocumentsProviderFullRestorePlugin(context, storage)
+ private val restorePlugin: RestorePlugin =
+ DocumentsProviderRestorePlugin(context, storage, kvRestorePlugin, fullRestorePlugin)
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() {
+ fun setup() = runBlocking {
every { mockedSettingsManager.getStorage() } returns settingsManager.getStorage()
- storage.rootBackupDir?.deleteContents()
+ storage.rootBackupDir?.deleteContents(context)
?: error("Select a storage location in the app first!")
}
@After
- fun tearDown() {
- storage.rootBackupDir?.deleteContents()
+ fun tearDown() = runBlocking {
+ storage.rootBackupDir?.deleteContents(context)
+ Unit
}
@Test
@@ -77,13 +98,12 @@ class PluginTest : KoinComponent {
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
+ // prepare returned tokens requested when initializing device
+ every { mockedSettingsManager.getToken() } returnsMany listOf(token, token + 1, token + 1)
- // device needs initialization, because new and storage is changing
- assertTrue(backupPlugin.initializeDevice(newToken = token))
+ // start new restore set and initialize device afterwards
+ backupPlugin.startNewRestoreSet(token)
+ backupPlugin.initializeDevice()
// write metadata (needed for backup to be recognized)
backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray())
@@ -92,22 +112,29 @@ class PluginTest : KoinComponent {
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))
+ // initializing again (with another restore set) does add a restore set
+ backupPlugin.startNewRestoreSet(token + 1)
+ backupPlugin.initializeDevice()
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))
+ // initializing again (without new restore set) doesn't change number of restore sets
+ backupPlugin.initializeDevice()
backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray())
assertEquals(2, restorePlugin.getAvailableBackups()?.toList()?.size)
+
+ // ensure that the new backup dirs exist
+ assertTrue(storage.currentKvBackupDir!!.exists())
+ assertTrue(storage.currentFullBackupDir!!.exists())
}
@Test
fun testMetadataWriteRead() = runBlocking(Dispatchers.IO) {
- every { mockedSettingsManager.getAndResetIsStorageChanging() } returns true andThen false
- assertTrue(backupPlugin.initializeDevice(newToken = token))
+ every { mockedSettingsManager.getToken() } returns token
+
+ backupPlugin.startNewRestoreSet(token)
+ backupPlugin.initializeDevice()
// write metadata
val metadata = getRandomByteArray()
@@ -124,7 +151,8 @@ class PluginTest : KoinComponent {
assertReadEquals(metadata, availableBackups[0].inputStream)
// initializing again (without changing storage) keeps restore set with same token
- assertFalse(backupPlugin.initializeDevice(newToken = token + 1))
+ backupPlugin.initializeDevice()
+ backupPlugin.getMetadataOutputStream().writeAndClose(metadata)
availableBackups = restorePlugin.getAvailableBackups()?.toList()
check(availableBackups != null)
assertEquals(1, availableBackups.size)
@@ -311,8 +339,8 @@ class PluginTest : KoinComponent {
}
private fun initStorage(token: Long) = runBlocking {
- every { mockedSettingsManager.getAndResetIsStorageChanging() } returns true
- assertTrue(backupPlugin.initializeDevice(newToken = token))
+ every { mockedSettingsManager.getToken() } returns token
+ backupPlugin.initializeDevice()
}
private fun isNextcloud(): Boolean {
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
index 5162ead7..3ea8999d 100644
--- a/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt
+++ b/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt
@@ -45,7 +45,7 @@ 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 storage = DocumentsStorage(context, settingsManager)
private val filename = getRandomBase64()
private lateinit var file: DocumentFile
@@ -96,6 +96,7 @@ class DocumentsStorageTest : KoinComponent {
val foundFile = storage.rootBackupDir!!.findFileBlocking(context, file.name!!)
assertNotNull(foundFile)
assertEquals(filename, foundFile!!.name)
+ assertEquals(storage.rootBackupDir!!.uri, foundFile.parentFile?.uri)
}
@Test
diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt
index 465ba7b8..afff3eed 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/App.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt
@@ -8,6 +8,7 @@ import android.os.Build
import android.os.ServiceManager.getService
import com.stevesoltys.seedvault.crypto.cryptoModule
import com.stevesoltys.seedvault.header.headerModule
+import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.metadataModule
import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule
import com.stevesoltys.seedvault.restore.RestoreViewModel
@@ -19,6 +20,7 @@ import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeViewModel
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
+import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.viewmodel.dsl.viewModel
@@ -39,7 +41,7 @@ class App : Application() {
viewModel { SettingsViewModel(this@App, get(), get(), get(), get()) }
viewModel { RecoveryCodeViewModel(this@App, get()) }
- viewModel { BackupStorageViewModel(this@App, get(), get()) }
+ viewModel { BackupStorageViewModel(this@App, get(), get(), get()) }
viewModel { RestoreStorageViewModel(this@App, get(), get()) }
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get()) }
}
@@ -49,7 +51,8 @@ class App : Application() {
startKoin {
androidLogger()
androidContext(this@App)
- modules(listOf(
+ modules(
+ listOf(
cryptoModule,
headerModule,
metadataModule,
@@ -57,7 +60,25 @@ class App : Application() {
backupModule,
restoreModule,
appModule
- ))
+ )
+ )
+ }
+ migrateTokenFromMetadataToSettingsManager()
+ }
+
+ private val settingsManager: SettingsManager by inject()
+ private val metadataManager: MetadataManager by inject()
+
+ /**
+ * The responsibility for the current token was moved to the [SettingsManager]
+ * in the end of 2020.
+ * This method migrates the token for existing installs and can be removed
+ * after sufficient time has passed.
+ */
+ private fun migrateTokenFromMetadataToSettingsManager() {
+ val token = metadataManager.getBackupToken()
+ if (token != 0L && settingsManager.getToken() == null) {
+ settingsManager.setNewToken(token)
}
}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt
index 03a6e079..bc3d37c5 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt
@@ -178,6 +178,7 @@ class MetadataManager(
* If the token is 0L, it is not yet initialized and must not be used for anything.
*/
@Synchronized
+ @Deprecated("Responsibility for current token moved to SettingsManager", ReplaceWith("settingsManager.getToken()"))
fun getBackupToken(): Long = metadata.token
/**
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 c3e38da3..0ba2a537 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
@@ -14,41 +14,34 @@ private const val MIME_TYPE_APK = "application/vnd.android.package-archive"
@Suppress("BlockingMethodInNonBlockingContext")
internal class DocumentsProviderBackupPlugin(
private val context: Context,
- private val storage: DocumentsStorage
+ private val storage: DocumentsStorage,
+ override val kvBackupPlugin: KVBackupPlugin,
+ override val fullBackupPlugin: FullBackupPlugin
) : BackupPlugin {
private val packageManager: PackageManager = context.packageManager
- override val kvBackupPlugin: KVBackupPlugin by lazy {
- DocumentsProviderKVBackup(storage, context)
- }
-
- override val fullBackupPlugin: FullBackupPlugin by lazy {
- DocumentsProviderFullBackup(storage, context)
- }
-
@Throws(IOException::class)
- override suspend fun initializeDevice(newToken: Long): Boolean {
- // check if storage is already initialized
- if (storage.isInitialized()) return false
-
- // TODO consider not creating new RestoreSets, but continue working within the existing one.
+ override suspend fun startNewRestoreSet(token: Long) {
// reset current storage
- storage.reset(newToken)
+ storage.reset(token)
// get or create root backup dir
storage.rootBackupDir ?: throw IOException()
+ }
+
+ @Throws(IOException::class)
+ override suspend fun initializeDevice() {
+ // wipe existing data
+ storage.getSetDir()?.deleteContents(context)
+
+ // reset storage without new token, so folders get recreated
+ // otherwise stale DocumentFiles will hang around
+ storage.reset(null)
// create backup folders
- val kvDir = storage.currentKvBackupDir
- val fullDir = storage.currentFullBackupDir
-
- // wipe existing data
- storage.getSetDir()?.findFileBlocking(context, FILE_BACKUP_METADATA)?.delete()
- kvDir?.deleteContents()
- fullDir?.deleteContents()
-
- return true
+ storage.currentKvBackupDir ?: throw IOException()
+ storage.currentFullBackupDir ?: throw IOException()
}
@Throws(IOException::class)
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 e2bef69e..5ef450bf 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
@@ -12,8 +12,8 @@ private val TAG = DocumentsProviderFullBackup::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class DocumentsProviderFullBackup(
- private val storage: DocumentsStorage,
- private val context: Context
+ private val context: Context,
+ private val storage: DocumentsStorage
) : FullBackupPlugin {
override fun getQuota() = DEFAULT_QUOTA_FULL_BACKUP
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 e81bcbd5..0da6066f 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
@@ -14,8 +14,8 @@ const val MAX_KEY_LENGTH_NEXTCLOUD = 225
@Suppress("BlockingMethodInNonBlockingContext")
internal class DocumentsProviderKVBackup(
- private val storage: DocumentsStorage,
- private val context: Context
+ private val context: Context,
+ private val storage: DocumentsStorage
) : KVBackupPlugin {
private var packageFile: DocumentFile? = null
@@ -27,7 +27,7 @@ internal class DocumentsProviderKVBackup(
val packageFile =
storage.currentKvBackupDir?.findFileBlocking(context, packageInfo.packageName)
?: return false
- return packageFile.listFiles().isNotEmpty()
+ return packageFile.listFilesBlocking(context).isNotEmpty()
}
@Throws(IOException::class)
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 d62f6952..be57ca24 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
@@ -26,10 +26,11 @@ internal class DocumentsProviderKVRestorePlugin(
}
}
- override fun listRecords(token: Long, packageInfo: PackageInfo): List {
+ @Throws(IOException::class)
+ override suspend fun listRecords(token: Long, packageInfo: PackageInfo): List {
val packageDir = this.packageDir ?: throw AssertionError()
packageDir.assertRightFile(packageInfo)
- return packageDir.listFiles()
+ return packageDir.listFilesBlocking(context)
.filter { file -> file.name != null }
.map { file -> file.name!! }
}
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 66b22b7f..f2c310af 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
@@ -1,12 +1,22 @@
package com.stevesoltys.seedvault.plugins.saf
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
+import com.stevesoltys.seedvault.transport.backup.FullBackupPlugin
+import com.stevesoltys.seedvault.transport.backup.KVBackupPlugin
+import com.stevesoltys.seedvault.transport.restore.FullRestorePlugin
+import com.stevesoltys.seedvault.transport.restore.KVRestorePlugin
import com.stevesoltys.seedvault.transport.restore.RestorePlugin
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val documentsProviderModule = module {
- single { DocumentsStorage(androidContext(), get(), get()) }
- single { DocumentsProviderBackupPlugin(androidContext(), get()) }
- single { DocumentsProviderRestorePlugin(androidContext(), get()) }
+ single { DocumentsStorage(androidContext(), get()) }
+
+ single { DocumentsProviderKVBackup(androidContext(), get()) }
+ single { DocumentsProviderFullBackup(androidContext(), get()) }
+ single { DocumentsProviderBackupPlugin(androidContext(), get(), get(), get()) }
+
+ single { DocumentsProviderKVRestorePlugin(androidContext(), get()) }
+ single { DocumentsProviderFullRestorePlugin(androidContext(), get()) }
+ single { DocumentsProviderRestorePlugin(androidContext(), get(), get(), 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 4da189d4..88af5399 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
@@ -19,17 +19,11 @@ private val TAG = DocumentsProviderRestorePlugin::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext") // all methods do I/O
internal class DocumentsProviderRestorePlugin(
private val context: Context,
- private val storage: DocumentsStorage
+ private val storage: DocumentsStorage,
+ override val kvRestorePlugin: KVRestorePlugin,
+ override val fullRestorePlugin: FullRestorePlugin
) : RestorePlugin {
- override val kvRestorePlugin: KVRestorePlugin by lazy {
- DocumentsProviderKVRestorePlugin(context, storage)
- }
-
- override val fullRestorePlugin: FullRestorePlugin by lazy {
- DocumentsProviderFullRestorePlugin(context, storage)
- }
-
@Throws(IOException::class)
override suspend fun hasBackup(uri: Uri): Boolean {
val parent = DocumentFile.fromTreeUri(context, uri) ?: throw AssertionError()
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 a76f76f8..d1e281d5 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,4 +1,4 @@
-@file:Suppress("EXPERIMENTAL_API_USAGE", "BlockingMethodInNonBlockingContext")
+@file:Suppress("BlockingMethodInNonBlockingContext")
package com.stevesoltys.seedvault.plugins.saf
@@ -9,17 +9,13 @@ 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
import android.provider.DocumentsContract.EXTRA_LOADING
import android.provider.DocumentsContract.buildChildDocumentsUriUsingTree
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 kotlinx.coroutines.TimeoutCancellationException
@@ -42,7 +38,6 @@ private val TAG = DocumentsStorage::class.java.simpleName
internal class DocumentsStorage(
private val context: Context,
- private val metadataManager: MetadataManager,
private val settingsManager: SettingsManager
) {
@@ -73,9 +68,9 @@ internal class DocumentsStorage(
field
}
- private var currentToken: Long = 0L
+ private var currentToken: Long? = null
get() {
- if (field == 0L) field = metadataManager.getBackupToken()
+ if (field == null) field = settingsManager.getToken()
return field
}
@@ -119,14 +114,10 @@ internal class DocumentsStorage(
field
}
- fun isInitialized(): Boolean {
- if (settingsManager.getAndResetIsStorageChanging()) return false // storage location has changed
- val kvEmpty = currentKvBackupDir?.listFiles()?.isEmpty() ?: false
- val fullEmpty = currentFullBackupDir?.listFiles()?.isEmpty() ?: false
- return kvEmpty && fullEmpty
- }
-
- fun reset(newToken: Long) {
+ /**
+ * Resets this storage abstraction, forcing it to re-fetch cached values on next access.
+ */
+ fun reset(newToken: Long?) {
storage = null
currentToken = newToken
rootBackupDir = null
@@ -138,26 +129,28 @@ internal class DocumentsStorage(
fun getAuthority(): String? = storage?.uri?.authority
@Throws(IOException::class)
- suspend fun getSetDir(token: Long = currentToken): DocumentFile? {
+ suspend fun getSetDir(token: Long = currentToken ?: error("no token")): DocumentFile? {
if (token == currentToken) return currentSetDir
return rootBackupDir?.findFileBlocking(context, token.toString())
}
@Throws(IOException::class)
- suspend fun getKVBackupDir(token: Long = currentToken): DocumentFile? {
+ suspend fun getKVBackupDir(token: Long = currentToken ?: error("no token")): DocumentFile? {
if (token == currentToken) return currentKvBackupDir ?: throw IOException()
return getSetDir(token)?.findFileBlocking(context, DIRECTORY_KEY_VALUE_BACKUP)
}
@Throws(IOException::class)
- suspend fun getOrCreateKVBackupDir(token: Long = currentToken): DocumentFile {
+ suspend fun getOrCreateKVBackupDir(
+ token: Long = currentToken ?: error("no token")
+ ): 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? {
+ suspend fun getFullBackupDir(token: Long = currentToken ?: error("no token")): DocumentFile? {
if (token == currentToken) return currentFullBackupDir ?: throw IOException()
return getSetDir(token)?.findFileBlocking(context, DIRECTORY_FULL_BACKUP)
}
@@ -195,12 +188,14 @@ internal suspend fun DocumentFile.createOrGetFile(
*/
@Throws(IOException::class)
suspend fun DocumentFile.createOrGetDirectory(context: Context, name: String): DocumentFile {
- return findFileBlocking(context, name) ?: createDirectory(name) ?: throw IOException()
+ return findFileBlocking(context, name) ?: createDirectory(name)?.apply {
+ check(this.name == name) { "Directory named ${this.name}, but should be $name" }
+ } ?: throw IOException()
}
@Throws(IOException::class)
-fun DocumentFile.deleteContents() {
- for (file in listFiles()) file.delete()
+suspend fun DocumentFile.deleteContents(context: Context) {
+ for (file in listFilesBlocking(context)) file.delete()
}
fun DocumentFile.assertRightFile(packageInfo: PackageInfo) {
@@ -214,10 +209,10 @@ fun DocumentFile.assertRightFile(packageInfo: PackageInfo) {
* This prevents getting an empty list even though there are children to be listed.
*/
@Throws(IOException::class)
-suspend fun DocumentFile.listFilesBlocking(context: Context): ArrayList {
+suspend fun DocumentFile.listFilesBlocking(context: Context): List {
val resolver = context.contentResolver
val childrenUri = buildChildDocumentsUriUsingTree(uri, getDocumentId(uri))
- val projection = arrayOf(COLUMN_DOCUMENT_ID, COLUMN_MIME_TYPE)
+ val projection = arrayOf(COLUMN_DOCUMENT_ID)
val result = ArrayList()
try {
@@ -229,20 +224,33 @@ suspend fun DocumentFile.listFilesBlocking(context: Context): ArrayList
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)!!
- } else {
- val documentUri = buildDocumentUriUsingTree(uri, documentId)
- DocumentFile.fromSingleUri(context, documentUri)!!
- }
- result.add(file)
+ val documentUri = buildDocumentUriUsingTree(uri, documentId)
+ result.add(getTreeDocumentFile(this, context, documentUri))
}
}
return result
}
+/**
+ * An extremely dirty reflection hack to instantiate a TreeDocumentFile with a parent.
+ *
+ * All other public ways to get a TreeDocumentFile only work from [Uri]s
+ * (e.g. [DocumentFile.fromTreeUri]) and always set parent to null.
+ *
+ * We have a test for this method to ensure CI will alert us when this reflection breaks.
+ * Also, [DocumentFile] is part of AndroidX, so we control the dependency and notice when it fails.
+ */
+@VisibleForTesting
+internal fun getTreeDocumentFile(parent: DocumentFile, context: Context, uri: Uri): DocumentFile {
+ @SuppressWarnings("MagicNumber")
+ val constructor = parent.javaClass.declaredConstructors.find {
+ it.name == "androidx.documentfile.provider.TreeDocumentFile" && it.parameterCount == 3
+ }
+ check(constructor != null) { "Could not find constructor for TreeDocumentFile" }
+ constructor.isAccessible = true
+ return constructor.newInstance(parent, context, uri) as DocumentFile
+}
+
/**
* Same as [DocumentFile.findFile] only that it re-queries when the first result was stale.
*
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 3336cc39..2ac3cb44 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt
@@ -1,16 +1,15 @@
package com.stevesoltys.seedvault.settings
-import android.app.backup.RestoreSet
import android.content.Context
import android.hardware.usb.UsbDevice
import android.net.Uri
import androidx.annotation.UiThread
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
-import com.stevesoltys.seedvault.transport.ConfigurableBackupTransport
+import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
import java.util.concurrent.ConcurrentSkipListSet
-import java.util.concurrent.atomic.AtomicBoolean
+internal const val PREF_KEY_TOKEN = "token"
internal const val PREF_KEY_BACKUP_APK = "backup_apk"
private const val PREF_KEY_STORAGE_URI = "storageUri"
@@ -28,7 +27,8 @@ class SettingsManager(context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
- private var isStorageChanging: AtomicBoolean = AtomicBoolean(false)
+ @Volatile
+ private var token: Long? = null
/**
* This gets accessed by non-UI threads when saving with [PreferenceManager]
@@ -39,6 +39,21 @@ class SettingsManager(context: Context) {
ConcurrentSkipListSet(prefs.getStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, emptySet()))
}
+ fun getToken(): Long? = token ?: {
+ val value = prefs.getLong(PREF_KEY_TOKEN, 0L)
+ if (value == 0L) null else value
+ }()
+
+ /**
+ * Sets a new RestoreSet token.
+ * Should only be called by the [BackupCoordinator]
+ * to ensure that related work is performed after moving to a new token.
+ */
+ fun setNewToken(newToken: Long) {
+ prefs.edit().putLong(PREF_KEY_TOKEN, newToken).apply()
+ token = newToken
+ }
+
// FIXME Storage is currently plugin specific and not generic
fun setStorage(storage: Storage) {
prefs.edit()
@@ -57,25 +72,6 @@ class SettingsManager(context: Context) {
return Storage(uri, name, isUsb)
}
- /**
- * When [ConfigurableBackupTransport.initializeDevice] we try to avoid deleting all stored data,
- * as this gets frequently called after network errors by SAF cloud providers.
- *
- * This method allows us to force a re-initialization of the underlying storage root
- * when we change to a new storage provider.
- * Currently, this causes us to create a new [RestoreSet].
- *
- * As part of the initialization, [getAndResetIsStorageChanging] should get called
- * to prevent future calls from causing re-initializations.
- */
- fun forceStorageInitialization() {
- isStorageChanging.set(true)
- }
-
- fun getAndResetIsStorageChanging(): Boolean {
- return isStorageChanging.getAndSet(false)
- }
-
fun setFlashDrive(usb: FlashDrive?) {
if (usb == null) {
prefs.edit()
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 12f40c98..af6cc574 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
@@ -4,13 +4,13 @@ import android.app.backup.BackupTransport.TRANSPORT_ERROR
import android.app.backup.BackupTransport.TRANSPORT_OK
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
+import android.app.backup.RestoreSet
import android.content.Context
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.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.metadata.MetadataManager
@@ -20,6 +20,7 @@ import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.settings.SettingsManager
+import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import java.io.IOException
import java.util.concurrent.TimeUnit.DAYS
@@ -52,6 +53,19 @@ internal class BackupCoordinator(
// Transport initialization and quota
//
+ /**
+ * Starts a new [RestoreSet] with a new token (the current unix epoch in milliseconds).
+ * Call this at least once before calling [initializeDevice]
+ * which must be called after this method to properly initialize the backup transport.
+ */
+ @Throws(IOException::class)
+ suspend fun startNewRestoreSet() {
+ val token = clock.time()
+ Log.i(TAG, "Starting new RestoreSet with token $token...")
+ settingsManager.setNewToken(token)
+ plugin.startNewRestoreSet(token)
+ }
+
/**
* Initialize the storage for this device, erasing all stored data.
* The transport may send the request immediately, or may buffer it.
@@ -70,28 +84,27 @@ internal class BackupCoordinator(
* @return One of [TRANSPORT_OK] (OK so far) or
* [TRANSPORT_ERROR] (to retry following network error or other failure).
*/
- suspend fun initializeDevice(): Int {
- Log.i(TAG, "Initialize Device!")
- return try {
- val token = clock.time()
- if (plugin.initializeDevice(token)) {
- Log.d(TAG, "Resetting backup metadata...")
- plugin.getMetadataOutputStream().use {
- metadataManager.onDeviceInitialization(token, it)
- }
- } else {
- Log.d(TAG, "Storage was already initialized, doing no-op")
+ suspend fun initializeDevice(): Int = try {
+ val token = settingsManager.getToken()
+ if (token == null) {
+ Log.i(TAG, "No RestoreSet started, initialization is no-op.")
+ } else {
+ Log.i(TAG, "Initialize Device!")
+ plugin.initializeDevice()
+ Log.d(TAG, "Resetting backup metadata for token $token...")
+ plugin.getMetadataOutputStream().use {
+ metadataManager.onDeviceInitialization(token, it)
}
- // [finishBackup] will only be called when we return [TRANSPORT_OK] here
- // so we remember that we initialized successfully
- calledInitialize = true
- TRANSPORT_OK
- } catch (e: IOException) {
- Log.e(TAG, "Error initializing device", e)
- // Show error notification if we were ready for backups
- if (getBackupBackoff() == 0L) nm.onBackupError()
- TRANSPORT_ERROR
}
+ // [finishBackup] will only be called when we return [TRANSPORT_OK] here
+ // so we remember that we initialized successfully
+ calledInitialize = true
+ TRANSPORT_OK
+ } catch (e: IOException) {
+ Log.e(TAG, "Error initializing device", e)
+ // Show error notification if we were ready for backups
+ if (getBackupBackoff() == 0L) nm.onBackupError()
+ TRANSPORT_ERROR
}
fun isAppEligibleForBackup(
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 c8d36a58..f6ffdbca 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
@@ -1,5 +1,6 @@
package com.stevesoltys.seedvault.transport.backup
+import android.app.backup.RestoreSet
import android.content.pm.PackageInfo
import java.io.IOException
import java.io.OutputStream
@@ -11,13 +12,18 @@ interface BackupPlugin {
val fullBackupPlugin: FullBackupPlugin
/**
- * Initialize the storage for this device, erasing all stored data.
+ * Start a new [RestoreSet] with the given token.
*
- * @return true if the device needs initialization or
- * false if the device was initialized already and initialization should be a no-op.
+ * This is typically followed by a call to [initializeDevice].
*/
@Throws(IOException::class)
- suspend fun initializeDevice(newToken: Long): Boolean
+ suspend fun startNewRestoreSet(token: Long)
+
+ /**
+ * Initialize the storage for this device, erasing all stored data in the current [RestoreSet].
+ */
+ @Throws(IOException::class)
+ suspend fun initializeDevice()
/**
* Returns an [OutputStream] for writing backup metadata.
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 ef86a4a8..04f11b3e 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
@@ -112,7 +112,7 @@ internal class KVRestore(
* Return a list of the records (represented by key files) in the given directory,
* sorted lexically by the Base64-decoded key file name, not by the on-disk filename.
*/
- private fun getSortedKeys(token: Long, packageInfo: PackageInfo): List? {
+ private suspend fun getSortedKeys(token: Long, packageInfo: PackageInfo): List? {
val records: List = try {
plugin.listRecords(token, packageInfo)
} catch (e: IOException) {
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 2e0d2f7f..fcf85065 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
@@ -20,7 +20,7 @@ interface KVRestorePlugin {
* For file-based plugins, this is usually a list of file names in the package directory.
*/
@Throws(IOException::class)
- fun listRecords(token: Long, packageInfo: PackageInfo): List
+ suspend fun listRecords(token: Long, packageInfo: PackageInfo): List
/**
* Return an [InputStream] for the given token, package and key
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 d0cb8363..2df92e93 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
@@ -13,7 +13,6 @@ import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor
import android.util.Log
import androidx.collection.LongSparseArray
-import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.header.UnsupportedVersionException
@@ -22,18 +21,19 @@ import com.stevesoltys.seedvault.metadata.DecryptionFailedException
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.MetadataReader
import com.stevesoltys.seedvault.settings.SettingsManager
+import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import libcore.io.IoUtils.closeQuietly
import java.io.IOException
private class RestoreCoordinatorState(
- internal val token: Long,
- internal val packages: Iterator,
+ val token: Long,
+ val packages: Iterator,
/**
* Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@
*/
- internal val pmPackageInfo: PackageInfo?
+ val pmPackageInfo: PackageInfo?
) {
- internal var currentPackage: String? = null
+ var currentPackage: String? = null
}
private val TAG = RestoreCoordinator::class.java.simpleName
@@ -106,8 +106,9 @@ internal class RestoreCoordinator(
* or 0 if there is no backup set available corresponding to the current device state.
*/
fun getCurrentRestoreSet(): Long {
- return metadataManager.getBackupToken()
- .apply { Log.i(TAG, "Got current restore set token: $this") }
+ return (settingsManager.getToken() ?: 0L).apply {
+ Log.i(TAG, "Got current restore set token: $this")
+ }
}
/**
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt
index b78b5613..617ab24a 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt
@@ -8,36 +8,51 @@ import android.net.Uri
import android.os.UserHandle
import android.util.Log
import androidx.annotation.WorkerThread
+import androidx.lifecycle.viewModelScope
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
+import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
import com.stevesoltys.seedvault.transport.requestBackup
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.io.IOException
private val TAG = BackupStorageViewModel::class.java.simpleName
internal class BackupStorageViewModel(
- private val app: Application,
- private val backupManager: IBackupManager,
- settingsManager: SettingsManager) : StorageViewModel(app, settingsManager) {
+ private val app: Application,
+ private val backupManager: IBackupManager,
+ private val backupCoordinator: BackupCoordinator,
+ settingsManager: SettingsManager
+) : StorageViewModel(app, settingsManager) {
override val isRestoreOperation = false
override fun onLocationSet(uri: Uri) {
val isUsb = saveStorage(uri)
- settingsManager.forceStorageInitialization()
+ viewModelScope.launch(Dispatchers.IO) {
+ try {
+ // will also generate a new backup token for the new restore set
+ backupCoordinator.startNewRestoreSet()
- // initialize the new location, will also generate a new backup token
- val observer = InitializationObserver()
- backupManager.initializeTransportsForUser(UserHandle.myUserId(), arrayOf(TRANSPORT_ID), observer)
-
- // if storage is on USB and this is not SetupWizard, do a backup right away
- if (isUsb && !isSetupWizard) Thread {
- requestBackup(app)
- }.start()
+ // initialize the new location
+ backupManager.initializeTransportsForUser(
+ UserHandle.myUserId(),
+ arrayOf(TRANSPORT_ID),
+ // if storage is on USB and this is not SetupWizard, do a backup right away
+ InitializationObserver(isUsb && !isSetupWizard)
+ )
+ } catch (e: IOException) {
+ Log.e(TAG, "Error starting new RestoreSet", e)
+ onInitializationError()
+ }
+ }
}
@WorkerThread
- private inner class InitializationObserver : IBackupObserver.Stub() {
+ private inner class InitializationObserver(val requestBackup: Boolean) :
+ IBackupObserver.Stub() {
override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) {
// noop
}
@@ -53,12 +68,19 @@ internal class BackupStorageViewModel(
if (status == 0) {
// notify the UI that the location has been set
mLocationChecked.postEvent(LocationResult())
+ if (requestBackup) {
+ requestBackup(app)
+ }
} else {
// notify the UI that the location was invalid
- val errorMsg = app.getString(R.string.storage_check_fragment_backup_error)
- mLocationChecked.postEvent(LocationResult(errorMsg))
+ onInitializationError()
}
}
}
+ private fun onInitializationError() {
+ val errorMsg = app.getString(R.string.storage_check_fragment_backup_error)
+ mLocationChecked.postEvent(LocationResult(errorMsg))
+ }
+
}
diff --git a/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/BackupPluginTest.kt b/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/BackupPluginTest.kt
new file mode 100644
index 00000000..61a56bd4
--- /dev/null
+++ b/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/BackupPluginTest.kt
@@ -0,0 +1,65 @@
+package com.stevesoltys.seedvault.plugins.saf
+
+import androidx.documentfile.provider.DocumentFile
+import com.stevesoltys.seedvault.transport.backup.BackupTest
+import com.stevesoltys.seedvault.transport.backup.FullBackupPlugin
+import com.stevesoltys.seedvault.transport.backup.KVBackupPlugin
+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.Test
+
+@Suppress("BlockingMethodInNonBlockingContext")
+internal class BackupPluginTest : BackupTest() {
+
+ private val storage = mockk()
+ private val kvBackupPlugin: KVBackupPlugin = mockk()
+ private val fullBackupPlugin: FullBackupPlugin = mockk()
+
+ private val plugin = DocumentsProviderBackupPlugin(
+ context,
+ storage,
+ kvBackupPlugin,
+ fullBackupPlugin
+ )
+
+ private val setDir: DocumentFile = mockk()
+ private val kvDir: DocumentFile = mockk()
+ private val fullDir: DocumentFile = mockk()
+
+ init {
+ // to mock extension functions on DocumentFile
+ mockkStatic("com.stevesoltys.seedvault.plugins.saf.DocumentsStorageKt")
+ }
+
+ @Test
+ fun `test startNewRestoreSet`() = runBlocking {
+ every { storage.reset(token) } just Runs
+ every { storage getProperty "rootBackupDir" } returns setDir
+
+ plugin.startNewRestoreSet(token)
+ }
+
+ @Test
+ fun `test initializeDevice`() = runBlocking {
+ // get current set dir and for that the current token
+ every { storage getProperty "currentToken" } returns token
+ every { settingsManager.getToken() } returns token
+ coEvery { storage.getSetDir(token) } returns setDir
+ // delete contents of current set dir
+ coEvery { setDir.listFilesBlocking(context) } returns listOf(kvDir)
+ every { kvDir.delete() } returns true
+ // reset storage
+ every { storage.reset(null) } just Runs
+ // create kv and full dir
+ every { storage getProperty "currentKvBackupDir" } returns kvDir
+ every { storage getProperty "currentFullBackupDir" } returns fullDir
+
+ plugin.initializeDevice()
+ }
+
+}
diff --git a/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/DocumentFileTest.kt b/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/DocumentFileTest.kt
new file mode 100644
index 00000000..47bddffb
--- /dev/null
+++ b/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/DocumentFileTest.kt
@@ -0,0 +1,43 @@
+package com.stevesoltys.seedvault.plugins.saf
+
+import android.content.Context
+import android.net.Uri
+import android.provider.DocumentsContract
+import androidx.documentfile.provider.DocumentFile
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.mockk.mockk
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.koin.core.context.stopKoin
+
+@RunWith(AndroidJUnit4::class)
+internal class DocumentFileTest {
+
+ private val context: Context = mockk()
+ private val parentUri: Uri = Uri.parse(
+ "content://com.android.externalstorage.documents/tree/" +
+ "primary%3A/document/primary%3A.SeedVaultAndroidBackup"
+ )
+ private val parentFile: DocumentFile = DocumentFile.fromTreeUri(context, parentUri)!!
+ private val uri: Uri = Uri.parse(
+ "content://com.android.externalstorage.documents/tree/" +
+ "primary%3A/document/primary%3A.SeedVaultAndroidBackup%2Ftest"
+ )
+
+ @After
+ fun afterEachTest() {
+ stopKoin()
+ }
+
+ @Test
+ fun `test ugly getTreeDocumentFile reflection hack`() {
+ assertTrue(DocumentsContract.isTreeUri(uri))
+ val file = getTreeDocumentFile(parentFile, context, uri)
+ assertEquals(uri, file.uri)
+ assertEquals(parentFile, file.parentFile)
+ }
+
+}
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 6d4a8fbb..26347c13 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt
@@ -7,7 +7,6 @@ import android.app.backup.BackupTransport.TRANSPORT_OK
import android.app.backup.RestoreDescription
import android.app.backup.RestoreDescription.TYPE_FULL_STREAM
import android.os.ParcelFileDescriptor
-import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
import com.stevesoltys.seedvault.crypto.CryptoImpl
import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
@@ -35,6 +34,7 @@ import com.stevesoltys.seedvault.transport.restore.KVRestorePlugin
import com.stevesoltys.seedvault.transport.restore.OutputFactory
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
import com.stevesoltys.seedvault.transport.restore.RestorePlugin
+import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.CapturingSlot
import io.mockk.Runs
import io.mockk.coEvery
@@ -186,7 +186,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
val backupDataOutput = mockk()
val rInputStream = ByteArrayInputStream(bOutputStream.toByteArray())
val rInputStream2 = ByteArrayInputStream(bOutputStream2.toByteArray())
- every { kvRestorePlugin.listRecords(token, packageInfo) } returns listOf(key64, key264)
+ coEvery { kvRestorePlugin.listRecords(token, packageInfo) } returns listOf(key64, key264)
every { outputFactory.getBackupDataOutput(fileDescriptor) } returns backupDataOutput
coEvery {
kvRestorePlugin.getInputStreamForRecord(
@@ -255,7 +255,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
// restore finds the backed up key and writes the decrypted value
val backupDataOutput = mockk()
val rInputStream = ByteArrayInputStream(bOutputStream.toByteArray())
- every { kvRestorePlugin.listRecords(token, packageInfo) } returns listOf(key64)
+ coEvery { kvRestorePlugin.listRecords(token, packageInfo) } returns listOf(key64)
every { outputFactory.getBackupDataOutput(fileDescriptor) } returns backupDataOutput
coEvery {
kvRestorePlugin.getInputStreamForRecord(
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 6bbe5c0f..8ed92841 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
@@ -8,7 +8,6 @@ import android.content.pm.PackageInfo
import android.net.Uri
import android.os.ParcelFileDescriptor
import androidx.documentfile.provider.DocumentFile
-import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.coAssertThrows
import com.stevesoltys.seedvault.getRandomString
@@ -18,6 +17,7 @@ import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.settings.Storage
+import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
@@ -63,9 +63,18 @@ internal class BackupCoordinatorTest : BackupTest() {
private val storage = Storage(Uri.EMPTY, getRandomString(), false)
@Test
- fun `device initialization succeeds and delegates to plugin`() = runBlocking {
+ fun `starting a new restore set works as expected`() = runBlocking {
every { clock.time() } returns token
- coEvery { plugin.initializeDevice(token) } returns true // TODO test when false
+ every { settingsManager.setNewToken(token) } just Runs
+ coEvery { plugin.startNewRestoreSet(token) } just Runs
+
+ backup.startNewRestoreSet()
+ }
+
+ @Test
+ fun `device initialization succeeds and delegates to plugin`() = runBlocking {
+ every { settingsManager.getToken() } returns token
+ coEvery { plugin.initializeDevice() } just Runs
coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onDeviceInitialization(token, metadataOutputStream) } just Runs
every { kv.hasState() } returns false
@@ -79,9 +88,8 @@ internal class BackupCoordinatorTest : BackupTest() {
}
@Test
- fun `device initialization does no-op when already initialized`() = runBlocking {
- every { clock.time() } returns token
- coEvery { plugin.initializeDevice(token) } returns false
+ fun `device initialization does no-op when no token available`() = runBlocking {
+ every { settingsManager.getToken() } returns null
every { kv.hasState() } returns false
every { full.hasState() } returns false
@@ -91,8 +99,8 @@ internal class BackupCoordinatorTest : BackupTest() {
@Test
fun `error notification when device initialization fails`() = runBlocking {
- every { clock.time() } returns token
- coEvery { plugin.initializeDevice(token) } throws IOException()
+ every { settingsManager.getToken() } returns token
+ coEvery { plugin.initializeDevice() } throws IOException()
every { settingsManager.getStorage() } returns storage
every { notificationManager.onBackupError() } just Runs
@@ -112,8 +120,8 @@ internal class BackupCoordinatorTest : BackupTest() {
val storage = mockk()
val documentFile = mockk()
- every { clock.time() } returns token
- coEvery { plugin.initializeDevice(token) } throws IOException()
+ every { settingsManager.getToken() } returns token
+ coEvery { plugin.initializeDevice() } throws IOException()
every { settingsManager.getStorage() } returns storage
every { storage.isUsb } returns true
every { storage.getDocumentFile(context) } returns documentFile
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 e63c69da..1298543c 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
@@ -56,7 +56,7 @@ internal class KVRestoreTest : RestoreTest() {
fun `listing records throws`() = runBlocking {
restore.initializeState(token, packageInfo)
- every { plugin.listRecords(token, packageInfo) } throws IOException()
+ coEvery { plugin.listRecords(token, packageInfo) } throws IOException()
assertEquals(TRANSPORT_ERROR, restore.getRestoreData(fileDescriptor))
}
@@ -208,7 +208,7 @@ internal class KVRestoreTest : RestoreTest() {
}
private fun getRecordsAndOutput(recordKeys: List = listOf(key64)) {
- every { plugin.listRecords(token, packageInfo) } returns recordKeys
+ coEvery { plugin.listRecords(token, packageInfo) } returns recordKeys
every { outputFactory.getBackupDataOutput(fileDescriptor) } returns output
}
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 6734e020..ac93ad51 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
@@ -9,7 +9,6 @@ import android.app.backup.RestoreDescription.TYPE_KEY_VALUE
import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor
import androidx.documentfile.provider.DocumentFile
-import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.coAssertThrows
import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.BackupMetadata
@@ -18,6 +17,7 @@ import com.stevesoltys.seedvault.metadata.MetadataReader
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.settings.Storage
import com.stevesoltys.seedvault.transport.TransportTest
+import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.every
@@ -91,7 +91,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
fun `getCurrentRestoreSet() delegates to plugin`() {
- every { metadataManager.getBackupToken() } returns token
+ every { settingsManager.getToken() } returns token
assertEquals(token, restore.getCurrentRestoreSet())
}