Clean up backup transport initialization logic
This commit makes creating new RestoreSets explicit. Initializing a backup transport now actually cleans its data as the AOSP documentation demands. This should be fine as we usually do a fresh backup after a new initialization. Contrary to before, an initialization does not create new RestoreSets anymore, but works within the existing set. For now, only manually choosing a new storage location creates a new RestoreSet.
This commit is contained in:
parent
80187c8c70
commit
1b9a4feddd
26 changed files with 454 additions and 195 deletions
48
.idea/runConfigurations/Instrumentation_Tests.xml
Normal file
48
.idea/runConfigurations/Instrumentation_Tests.xml
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Instrumentation Tests" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
|
||||||
|
<module name="app" />
|
||||||
|
<option name="TESTING_TYPE" value="0" />
|
||||||
|
<option name="METHOD_NAME" value="" />
|
||||||
|
<option name="CLASS_NAME" value="" />
|
||||||
|
<option name="PACKAGE_NAME" value="" />
|
||||||
|
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
|
||||||
|
<option name="EXTRA_OPTIONS" value="-e notAnnotation androidx.test.filters.LargeTest" />
|
||||||
|
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
|
||||||
|
<option name="CLEAR_LOGCAT" value="false" />
|
||||||
|
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
|
||||||
|
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
|
||||||
|
<option name="FORCE_STOP_RUNNING_APP" value="true" />
|
||||||
|
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
||||||
|
<option name="DEBUGGER_TYPE" value="Auto" />
|
||||||
|
<Auto>
|
||||||
|
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||||
|
<option name="SHOW_STATIC_VARS" value="true" />
|
||||||
|
<option name="WORKING_DIR" value="" />
|
||||||
|
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||||
|
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||||
|
</Auto>
|
||||||
|
<Hybrid>
|
||||||
|
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||||
|
<option name="SHOW_STATIC_VARS" value="true" />
|
||||||
|
<option name="WORKING_DIR" value="" />
|
||||||
|
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||||
|
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||||
|
</Hybrid>
|
||||||
|
<Java />
|
||||||
|
<Native>
|
||||||
|
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
||||||
|
<option name="SHOW_STATIC_VARS" value="true" />
|
||||||
|
<option name="WORKING_DIR" value="" />
|
||||||
|
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
||||||
|
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
||||||
|
</Native>
|
||||||
|
<Profilers>
|
||||||
|
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
|
||||||
|
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
|
||||||
|
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
|
||||||
|
</Profilers>
|
||||||
|
<method v="2">
|
||||||
|
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
|
@ -1,8 +1,8 @@
|
||||||
package com.stevesoltys.seedvault
|
package com.stevesoltys.seedvault
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import androidx.test.runner.AndroidJUnit4
|
|
||||||
import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
|
import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
|
import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
|
|
|
@ -3,8 +3,11 @@ package com.stevesoltys.seedvault
|
||||||
import androidx.test.core.content.pm.PackageInfoBuilder
|
import androidx.test.core.content.pm.PackageInfoBuilder
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
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.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.DocumentsProviderRestorePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
|
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
|
||||||
import com.stevesoltys.seedvault.plugins.saf.MAX_KEY_LENGTH
|
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.plugins.saf.deleteContents
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
|
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 com.stevesoltys.seedvault.transport.restore.RestorePlugin
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
@ -35,27 +42,41 @@ import kotlin.random.Random
|
||||||
class PluginTest : KoinComponent {
|
class PluginTest : KoinComponent {
|
||||||
|
|
||||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
private val metadataManager: MetadataManager by inject()
|
|
||||||
private val settingsManager: SettingsManager by inject()
|
private val settingsManager: SettingsManager by inject()
|
||||||
private val mockedSettingsManager: SettingsManager = mockk()
|
private val mockedSettingsManager: SettingsManager = mockk()
|
||||||
private val storage = DocumentsStorage(context, metadataManager, mockedSettingsManager)
|
private val storage = DocumentsStorage(context, mockedSettingsManager)
|
||||||
private val backupPlugin: BackupPlugin = DocumentsProviderBackupPlugin(context, storage)
|
|
||||||
private val restorePlugin: RestorePlugin = DocumentsProviderRestorePlugin(context, storage)
|
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 token = Random.nextLong()
|
||||||
private val packageInfo = PackageInfoBuilder.newBuilder().setPackageName("org.example").build()
|
private val packageInfo = PackageInfoBuilder.newBuilder().setPackageName("org.example").build()
|
||||||
private val packageInfo2 = PackageInfoBuilder.newBuilder().setPackageName("net.example").build()
|
private val packageInfo2 = PackageInfoBuilder.newBuilder().setPackageName("net.example").build()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() = runBlocking {
|
||||||
every { mockedSettingsManager.getStorage() } returns settingsManager.getStorage()
|
every { mockedSettingsManager.getStorage() } returns settingsManager.getStorage()
|
||||||
storage.rootBackupDir?.deleteContents()
|
storage.rootBackupDir?.deleteContents(context)
|
||||||
?: error("Select a storage location in the app first!")
|
?: error("Select a storage location in the app first!")
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() = runBlocking {
|
||||||
storage.rootBackupDir?.deleteContents()
|
storage.rootBackupDir?.deleteContents(context)
|
||||||
|
Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -77,13 +98,12 @@ class PluginTest : KoinComponent {
|
||||||
val uri = settingsManager.getStorage()?.getDocumentFile(context)?.uri ?: error("no storage")
|
val uri = settingsManager.getStorage()?.getDocumentFile(context)?.uri ?: error("no storage")
|
||||||
assertFalse(restorePlugin.hasBackup(uri))
|
assertFalse(restorePlugin.hasBackup(uri))
|
||||||
|
|
||||||
// define storage changing state for later
|
// prepare returned tokens requested when initializing device
|
||||||
every {
|
every { mockedSettingsManager.getToken() } returnsMany listOf(token, token + 1, token + 1)
|
||||||
mockedSettingsManager.getAndResetIsStorageChanging()
|
|
||||||
} returns true andThen true andThen false
|
|
||||||
|
|
||||||
// device needs initialization, because new and storage is changing
|
// start new restore set and initialize device afterwards
|
||||||
assertTrue(backupPlugin.initializeDevice(newToken = token))
|
backupPlugin.startNewRestoreSet(token)
|
||||||
|
backupPlugin.initializeDevice()
|
||||||
|
|
||||||
// write metadata (needed for backup to be recognized)
|
// write metadata (needed for backup to be recognized)
|
||||||
backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray())
|
backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray())
|
||||||
|
@ -92,22 +112,29 @@ class PluginTest : KoinComponent {
|
||||||
assertEquals(1, restorePlugin.getAvailableBackups()?.toList()?.size)
|
assertEquals(1, restorePlugin.getAvailableBackups()?.toList()?.size)
|
||||||
assertTrue(restorePlugin.hasBackup(uri))
|
assertTrue(restorePlugin.hasBackup(uri))
|
||||||
|
|
||||||
// initializing again (while changing storage) does add a restore set
|
// initializing again (with another restore set) does add a restore set
|
||||||
assertTrue(backupPlugin.initializeDevice(newToken = token + 1))
|
backupPlugin.startNewRestoreSet(token + 1)
|
||||||
|
backupPlugin.initializeDevice()
|
||||||
backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray())
|
backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray())
|
||||||
assertEquals(2, restorePlugin.getAvailableBackups()?.toList()?.size)
|
assertEquals(2, restorePlugin.getAvailableBackups()?.toList()?.size)
|
||||||
assertTrue(restorePlugin.hasBackup(uri))
|
assertTrue(restorePlugin.hasBackup(uri))
|
||||||
|
|
||||||
// initializing again (without changing storage) doesn't change number of restore sets
|
// initializing again (without new restore set) doesn't change number of restore sets
|
||||||
assertFalse(backupPlugin.initializeDevice(newToken = token + 2))
|
backupPlugin.initializeDevice()
|
||||||
backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray())
|
backupPlugin.getMetadataOutputStream().writeAndClose(getRandomByteArray())
|
||||||
assertEquals(2, restorePlugin.getAvailableBackups()?.toList()?.size)
|
assertEquals(2, restorePlugin.getAvailableBackups()?.toList()?.size)
|
||||||
|
|
||||||
|
// ensure that the new backup dirs exist
|
||||||
|
assertTrue(storage.currentKvBackupDir!!.exists())
|
||||||
|
assertTrue(storage.currentFullBackupDir!!.exists())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMetadataWriteRead() = runBlocking(Dispatchers.IO) {
|
fun testMetadataWriteRead() = runBlocking(Dispatchers.IO) {
|
||||||
every { mockedSettingsManager.getAndResetIsStorageChanging() } returns true andThen false
|
every { mockedSettingsManager.getToken() } returns token
|
||||||
assertTrue(backupPlugin.initializeDevice(newToken = token))
|
|
||||||
|
backupPlugin.startNewRestoreSet(token)
|
||||||
|
backupPlugin.initializeDevice()
|
||||||
|
|
||||||
// write metadata
|
// write metadata
|
||||||
val metadata = getRandomByteArray()
|
val metadata = getRandomByteArray()
|
||||||
|
@ -124,7 +151,8 @@ class PluginTest : KoinComponent {
|
||||||
assertReadEquals(metadata, availableBackups[0].inputStream)
|
assertReadEquals(metadata, availableBackups[0].inputStream)
|
||||||
|
|
||||||
// initializing again (without changing storage) keeps restore set with same token
|
// 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()
|
availableBackups = restorePlugin.getAvailableBackups()?.toList()
|
||||||
check(availableBackups != null)
|
check(availableBackups != null)
|
||||||
assertEquals(1, availableBackups.size)
|
assertEquals(1, availableBackups.size)
|
||||||
|
@ -311,8 +339,8 @@ class PluginTest : KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initStorage(token: Long) = runBlocking {
|
private fun initStorage(token: Long) = runBlocking {
|
||||||
every { mockedSettingsManager.getAndResetIsStorageChanging() } returns true
|
every { mockedSettingsManager.getToken() } returns token
|
||||||
assertTrue(backupPlugin.initializeDevice(newToken = token))
|
backupPlugin.initializeDevice()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isNextcloud(): Boolean {
|
private fun isNextcloud(): Boolean {
|
||||||
|
|
|
@ -45,7 +45,7 @@ class DocumentsStorageTest : KoinComponent {
|
||||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
private val metadataManager by inject<MetadataManager>()
|
private val metadataManager by inject<MetadataManager>()
|
||||||
private val settingsManager by inject<SettingsManager>()
|
private val settingsManager by inject<SettingsManager>()
|
||||||
private val storage = DocumentsStorage(context, metadataManager, settingsManager)
|
private val storage = DocumentsStorage(context, settingsManager)
|
||||||
|
|
||||||
private val filename = getRandomBase64()
|
private val filename = getRandomBase64()
|
||||||
private lateinit var file: DocumentFile
|
private lateinit var file: DocumentFile
|
||||||
|
@ -96,6 +96,7 @@ class DocumentsStorageTest : KoinComponent {
|
||||||
val foundFile = storage.rootBackupDir!!.findFileBlocking(context, file.name!!)
|
val foundFile = storage.rootBackupDir!!.findFileBlocking(context, file.name!!)
|
||||||
assertNotNull(foundFile)
|
assertNotNull(foundFile)
|
||||||
assertEquals(filename, foundFile!!.name)
|
assertEquals(filename, foundFile!!.name)
|
||||||
|
assertEquals(storage.rootBackupDir!!.uri, foundFile.parentFile?.uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -8,6 +8,7 @@ import android.os.Build
|
||||||
import android.os.ServiceManager.getService
|
import android.os.ServiceManager.getService
|
||||||
import com.stevesoltys.seedvault.crypto.cryptoModule
|
import com.stevesoltys.seedvault.crypto.cryptoModule
|
||||||
import com.stevesoltys.seedvault.header.headerModule
|
import com.stevesoltys.seedvault.header.headerModule
|
||||||
|
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
import com.stevesoltys.seedvault.metadata.metadataModule
|
import com.stevesoltys.seedvault.metadata.metadataModule
|
||||||
import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule
|
import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule
|
||||||
import com.stevesoltys.seedvault.restore.RestoreViewModel
|
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.recoverycode.RecoveryCodeViewModel
|
||||||
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
|
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
|
||||||
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
|
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.androidContext
|
||||||
import org.koin.android.ext.koin.androidLogger
|
import org.koin.android.ext.koin.androidLogger
|
||||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
@ -39,7 +41,7 @@ class App : Application() {
|
||||||
|
|
||||||
viewModel { SettingsViewModel(this@App, get(), get(), get(), get()) }
|
viewModel { SettingsViewModel(this@App, get(), get(), get(), get()) }
|
||||||
viewModel { RecoveryCodeViewModel(this@App, 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 { RestoreStorageViewModel(this@App, get(), get()) }
|
||||||
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get()) }
|
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get()) }
|
||||||
}
|
}
|
||||||
|
@ -49,7 +51,8 @@ class App : Application() {
|
||||||
startKoin {
|
startKoin {
|
||||||
androidLogger()
|
androidLogger()
|
||||||
androidContext(this@App)
|
androidContext(this@App)
|
||||||
modules(listOf(
|
modules(
|
||||||
|
listOf(
|
||||||
cryptoModule,
|
cryptoModule,
|
||||||
headerModule,
|
headerModule,
|
||||||
metadataModule,
|
metadataModule,
|
||||||
|
@ -57,7 +60,25 @@ class App : Application() {
|
||||||
backupModule,
|
backupModule,
|
||||||
restoreModule,
|
restoreModule,
|
||||||
appModule
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -178,6 +178,7 @@ class MetadataManager(
|
||||||
* If the token is 0L, it is not yet initialized and must not be used for anything.
|
* If the token is 0L, it is not yet initialized and must not be used for anything.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
@Deprecated("Responsibility for current token moved to SettingsManager", ReplaceWith("settingsManager.getToken()"))
|
||||||
fun getBackupToken(): Long = metadata.token
|
fun getBackupToken(): Long = metadata.token
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -14,41 +14,34 @@ private const val MIME_TYPE_APK = "application/vnd.android.package-archive"
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class DocumentsProviderBackupPlugin(
|
internal class DocumentsProviderBackupPlugin(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val storage: DocumentsStorage
|
private val storage: DocumentsStorage,
|
||||||
|
override val kvBackupPlugin: KVBackupPlugin,
|
||||||
|
override val fullBackupPlugin: FullBackupPlugin
|
||||||
) : BackupPlugin {
|
) : BackupPlugin {
|
||||||
|
|
||||||
private val packageManager: PackageManager = context.packageManager
|
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)
|
@Throws(IOException::class)
|
||||||
override suspend fun initializeDevice(newToken: Long): Boolean {
|
override suspend fun startNewRestoreSet(token: Long) {
|
||||||
// check if storage is already initialized
|
|
||||||
if (storage.isInitialized()) return false
|
|
||||||
|
|
||||||
// TODO consider not creating new RestoreSets, but continue working within the existing one.
|
|
||||||
// reset current storage
|
// reset current storage
|
||||||
storage.reset(newToken)
|
storage.reset(token)
|
||||||
|
|
||||||
// get or create root backup dir
|
// get or create root backup dir
|
||||||
storage.rootBackupDir ?: throw IOException()
|
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
|
// create backup folders
|
||||||
val kvDir = storage.currentKvBackupDir
|
storage.currentKvBackupDir ?: throw IOException()
|
||||||
val fullDir = storage.currentFullBackupDir
|
storage.currentFullBackupDir ?: throw IOException()
|
||||||
|
|
||||||
// wipe existing data
|
|
||||||
storage.getSetDir()?.findFileBlocking(context, FILE_BACKUP_METADATA)?.delete()
|
|
||||||
kvDir?.deleteContents()
|
|
||||||
fullDir?.deleteContents()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
|
|
|
@ -12,8 +12,8 @@ private val TAG = DocumentsProviderFullBackup::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class DocumentsProviderFullBackup(
|
internal class DocumentsProviderFullBackup(
|
||||||
private val storage: DocumentsStorage,
|
private val context: Context,
|
||||||
private val context: Context
|
private val storage: DocumentsStorage
|
||||||
) : FullBackupPlugin {
|
) : FullBackupPlugin {
|
||||||
|
|
||||||
override fun getQuota() = DEFAULT_QUOTA_FULL_BACKUP
|
override fun getQuota() = DEFAULT_QUOTA_FULL_BACKUP
|
||||||
|
|
|
@ -14,8 +14,8 @@ const val MAX_KEY_LENGTH_NEXTCLOUD = 225
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class DocumentsProviderKVBackup(
|
internal class DocumentsProviderKVBackup(
|
||||||
private val storage: DocumentsStorage,
|
private val context: Context,
|
||||||
private val context: Context
|
private val storage: DocumentsStorage
|
||||||
) : KVBackupPlugin {
|
) : KVBackupPlugin {
|
||||||
|
|
||||||
private var packageFile: DocumentFile? = null
|
private var packageFile: DocumentFile? = null
|
||||||
|
@ -27,7 +27,7 @@ internal class DocumentsProviderKVBackup(
|
||||||
val packageFile =
|
val packageFile =
|
||||||
storage.currentKvBackupDir?.findFileBlocking(context, packageInfo.packageName)
|
storage.currentKvBackupDir?.findFileBlocking(context, packageInfo.packageName)
|
||||||
?: return false
|
?: return false
|
||||||
return packageFile.listFiles().isNotEmpty()
|
return packageFile.listFilesBlocking(context).isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
|
|
|
@ -26,10 +26,11 @@ internal class DocumentsProviderKVRestorePlugin(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun listRecords(token: Long, packageInfo: PackageInfo): List<String> {
|
@Throws(IOException::class)
|
||||||
|
override suspend fun listRecords(token: Long, packageInfo: PackageInfo): List<String> {
|
||||||
val packageDir = this.packageDir ?: throw AssertionError()
|
val packageDir = this.packageDir ?: throw AssertionError()
|
||||||
packageDir.assertRightFile(packageInfo)
|
packageDir.assertRightFile(packageInfo)
|
||||||
return packageDir.listFiles()
|
return packageDir.listFilesBlocking(context)
|
||||||
.filter { file -> file.name != null }
|
.filter { file -> file.name != null }
|
||||||
.map { file -> file.name!! }
|
.map { file -> file.name!! }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,22 @@
|
||||||
package com.stevesoltys.seedvault.plugins.saf
|
package com.stevesoltys.seedvault.plugins.saf
|
||||||
|
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
|
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 com.stevesoltys.seedvault.transport.restore.RestorePlugin
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val documentsProviderModule = module {
|
val documentsProviderModule = module {
|
||||||
single { DocumentsStorage(androidContext(), get(), get()) }
|
single { DocumentsStorage(androidContext(), get()) }
|
||||||
single<BackupPlugin> { DocumentsProviderBackupPlugin(androidContext(), get()) }
|
|
||||||
single<RestorePlugin> { DocumentsProviderRestorePlugin(androidContext(), get()) }
|
single<KVBackupPlugin> { DocumentsProviderKVBackup(androidContext(), get()) }
|
||||||
|
single<FullBackupPlugin> { DocumentsProviderFullBackup(androidContext(), get()) }
|
||||||
|
single<BackupPlugin> { DocumentsProviderBackupPlugin(androidContext(), get(), get(), get()) }
|
||||||
|
|
||||||
|
single<KVRestorePlugin> { DocumentsProviderKVRestorePlugin(androidContext(), get()) }
|
||||||
|
single<FullRestorePlugin> { DocumentsProviderFullRestorePlugin(androidContext(), get()) }
|
||||||
|
single<RestorePlugin> { DocumentsProviderRestorePlugin(androidContext(), get(), get(), get()) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,17 +19,11 @@ private val TAG = DocumentsProviderRestorePlugin::class.java.simpleName
|
||||||
@Suppress("BlockingMethodInNonBlockingContext") // all methods do I/O
|
@Suppress("BlockingMethodInNonBlockingContext") // all methods do I/O
|
||||||
internal class DocumentsProviderRestorePlugin(
|
internal class DocumentsProviderRestorePlugin(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val storage: DocumentsStorage
|
private val storage: DocumentsStorage,
|
||||||
|
override val kvRestorePlugin: KVRestorePlugin,
|
||||||
|
override val fullRestorePlugin: FullRestorePlugin
|
||||||
) : RestorePlugin {
|
) : RestorePlugin {
|
||||||
|
|
||||||
override val kvRestorePlugin: KVRestorePlugin by lazy {
|
|
||||||
DocumentsProviderKVRestorePlugin(context, storage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val fullRestorePlugin: FullRestorePlugin by lazy {
|
|
||||||
DocumentsProviderFullRestorePlugin(context, storage)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override suspend fun hasBackup(uri: Uri): Boolean {
|
override suspend fun hasBackup(uri: Uri): Boolean {
|
||||||
val parent = DocumentFile.fromTreeUri(context, uri) ?: throw AssertionError()
|
val parent = DocumentFile.fromTreeUri(context, uri) ?: throw AssertionError()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@file:Suppress("EXPERIMENTAL_API_USAGE", "BlockingMethodInNonBlockingContext")
|
@file:Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
|
||||||
package com.stevesoltys.seedvault.plugins.saf
|
package com.stevesoltys.seedvault.plugins.saf
|
||||||
|
|
||||||
|
@ -9,17 +9,13 @@ import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.FileUtils.closeQuietly
|
import android.os.FileUtils.closeQuietly
|
||||||
import android.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID
|
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.EXTRA_LOADING
|
||||||
import android.provider.DocumentsContract.buildChildDocumentsUriUsingTree
|
import android.provider.DocumentsContract.buildChildDocumentsUriUsingTree
|
||||||
import android.provider.DocumentsContract.buildDocumentUriUsingTree
|
import android.provider.DocumentsContract.buildDocumentUriUsingTree
|
||||||
import android.provider.DocumentsContract.buildTreeDocumentUri
|
|
||||||
import android.provider.DocumentsContract.getDocumentId
|
import android.provider.DocumentsContract.getDocumentId
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.settings.Storage
|
import com.stevesoltys.seedvault.settings.Storage
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
|
@ -42,7 +38,6 @@ private val TAG = DocumentsStorage::class.java.simpleName
|
||||||
|
|
||||||
internal class DocumentsStorage(
|
internal class DocumentsStorage(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val metadataManager: MetadataManager,
|
|
||||||
private val settingsManager: SettingsManager
|
private val settingsManager: SettingsManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -73,9 +68,9 @@ internal class DocumentsStorage(
|
||||||
field
|
field
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentToken: Long = 0L
|
private var currentToken: Long? = null
|
||||||
get() {
|
get() {
|
||||||
if (field == 0L) field = metadataManager.getBackupToken()
|
if (field == null) field = settingsManager.getToken()
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,14 +114,10 @@ internal class DocumentsStorage(
|
||||||
field
|
field
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isInitialized(): Boolean {
|
/**
|
||||||
if (settingsManager.getAndResetIsStorageChanging()) return false // storage location has changed
|
* Resets this storage abstraction, forcing it to re-fetch cached values on next access.
|
||||||
val kvEmpty = currentKvBackupDir?.listFiles()?.isEmpty() ?: false
|
*/
|
||||||
val fullEmpty = currentFullBackupDir?.listFiles()?.isEmpty() ?: false
|
fun reset(newToken: Long?) {
|
||||||
return kvEmpty && fullEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset(newToken: Long) {
|
|
||||||
storage = null
|
storage = null
|
||||||
currentToken = newToken
|
currentToken = newToken
|
||||||
rootBackupDir = null
|
rootBackupDir = null
|
||||||
|
@ -138,26 +129,28 @@ internal class DocumentsStorage(
|
||||||
fun getAuthority(): String? = storage?.uri?.authority
|
fun getAuthority(): String? = storage?.uri?.authority
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@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
|
if (token == currentToken) return currentSetDir
|
||||||
return rootBackupDir?.findFileBlocking(context, token.toString())
|
return rootBackupDir?.findFileBlocking(context, token.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@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()
|
if (token == currentToken) return currentKvBackupDir ?: throw IOException()
|
||||||
return getSetDir(token)?.findFileBlocking(context, DIRECTORY_KEY_VALUE_BACKUP)
|
return getSetDir(token)?.findFileBlocking(context, DIRECTORY_KEY_VALUE_BACKUP)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@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()
|
if (token == currentToken) return currentKvBackupDir ?: throw IOException()
|
||||||
val setDir = getSetDir(token) ?: throw IOException()
|
val setDir = getSetDir(token) ?: throw IOException()
|
||||||
return setDir.createOrGetDirectory(context, DIRECTORY_KEY_VALUE_BACKUP)
|
return setDir.createOrGetDirectory(context, DIRECTORY_KEY_VALUE_BACKUP)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@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()
|
if (token == currentToken) return currentFullBackupDir ?: throw IOException()
|
||||||
return getSetDir(token)?.findFileBlocking(context, DIRECTORY_FULL_BACKUP)
|
return getSetDir(token)?.findFileBlocking(context, DIRECTORY_FULL_BACKUP)
|
||||||
}
|
}
|
||||||
|
@ -195,12 +188,14 @@ internal suspend fun DocumentFile.createOrGetFile(
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
suspend fun DocumentFile.createOrGetDirectory(context: Context, name: String): DocumentFile {
|
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)
|
@Throws(IOException::class)
|
||||||
fun DocumentFile.deleteContents() {
|
suspend fun DocumentFile.deleteContents(context: Context) {
|
||||||
for (file in listFiles()) file.delete()
|
for (file in listFilesBlocking(context)) file.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DocumentFile.assertRightFile(packageInfo: PackageInfo) {
|
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.
|
* This prevents getting an empty list even though there are children to be listed.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
suspend fun DocumentFile.listFilesBlocking(context: Context): ArrayList<DocumentFile> {
|
suspend fun DocumentFile.listFilesBlocking(context: Context): List<DocumentFile> {
|
||||||
val resolver = context.contentResolver
|
val resolver = context.contentResolver
|
||||||
val childrenUri = buildChildDocumentsUriUsingTree(uri, getDocumentId(uri))
|
val childrenUri = buildChildDocumentsUriUsingTree(uri, getDocumentId(uri))
|
||||||
val projection = arrayOf(COLUMN_DOCUMENT_ID, COLUMN_MIME_TYPE)
|
val projection = arrayOf(COLUMN_DOCUMENT_ID)
|
||||||
val result = ArrayList<DocumentFile>()
|
val result = ArrayList<DocumentFile>()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -229,20 +224,33 @@ suspend fun DocumentFile.listFilesBlocking(context: Context): ArrayList<Document
|
||||||
}.use { cursor ->
|
}.use { cursor ->
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val documentId = cursor.getString(0)
|
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)
|
val documentUri = buildDocumentUriUsingTree(uri, documentId)
|
||||||
DocumentFile.fromSingleUri(context, documentUri)!!
|
result.add(getTreeDocumentFile(this, context, documentUri))
|
||||||
}
|
|
||||||
result.add(file)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
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.
|
* Same as [DocumentFile.findFile] only that it re-queries when the first result was stale.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
package com.stevesoltys.seedvault.settings
|
package com.stevesoltys.seedvault.settings
|
||||||
|
|
||||||
import android.app.backup.RestoreSet
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.hardware.usb.UsbDevice
|
import android.hardware.usb.UsbDevice
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.annotation.UiThread
|
import androidx.annotation.UiThread
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.preference.PreferenceManager
|
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.ConcurrentSkipListSet
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
|
internal const val PREF_KEY_TOKEN = "token"
|
||||||
internal const val PREF_KEY_BACKUP_APK = "backup_apk"
|
internal const val PREF_KEY_BACKUP_APK = "backup_apk"
|
||||||
|
|
||||||
private const val PREF_KEY_STORAGE_URI = "storageUri"
|
private const val PREF_KEY_STORAGE_URI = "storageUri"
|
||||||
|
@ -28,7 +27,8 @@ class SettingsManager(context: Context) {
|
||||||
|
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(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]
|
* 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()))
|
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
|
// FIXME Storage is currently plugin specific and not generic
|
||||||
fun setStorage(storage: Storage) {
|
fun setStorage(storage: Storage) {
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
|
@ -57,25 +72,6 @@ class SettingsManager(context: Context) {
|
||||||
return Storage(uri, name, isUsb)
|
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?) {
|
fun setFlashDrive(usb: FlashDrive?) {
|
||||||
if (usb == null) {
|
if (usb == null) {
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
|
|
|
@ -4,13 +4,13 @@ import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
|
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
|
||||||
|
import android.app.backup.RestoreSet
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
|
||||||
import com.stevesoltys.seedvault.Clock
|
import com.stevesoltys.seedvault.Clock
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
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.QUOTA_EXCEEDED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit.DAYS
|
import java.util.concurrent.TimeUnit.DAYS
|
||||||
|
|
||||||
|
@ -52,6 +53,19 @@ internal class BackupCoordinator(
|
||||||
// Transport initialization and quota
|
// 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.
|
* Initialize the storage for this device, erasing all stored data.
|
||||||
* The transport may send the request immediately, or may buffer it.
|
* The transport may send the request immediately, or may buffer it.
|
||||||
|
@ -70,17 +84,17 @@ internal class BackupCoordinator(
|
||||||
* @return One of [TRANSPORT_OK] (OK so far) or
|
* @return One of [TRANSPORT_OK] (OK so far) or
|
||||||
* [TRANSPORT_ERROR] (to retry following network error or other failure).
|
* [TRANSPORT_ERROR] (to retry following network error or other failure).
|
||||||
*/
|
*/
|
||||||
suspend fun initializeDevice(): Int {
|
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!")
|
Log.i(TAG, "Initialize Device!")
|
||||||
return try {
|
plugin.initializeDevice()
|
||||||
val token = clock.time()
|
Log.d(TAG, "Resetting backup metadata for token $token...")
|
||||||
if (plugin.initializeDevice(token)) {
|
|
||||||
Log.d(TAG, "Resetting backup metadata...")
|
|
||||||
plugin.getMetadataOutputStream().use {
|
plugin.getMetadataOutputStream().use {
|
||||||
metadataManager.onDeviceInitialization(token, it)
|
metadataManager.onDeviceInitialization(token, it)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Log.d(TAG, "Storage was already initialized, doing no-op")
|
|
||||||
}
|
}
|
||||||
// [finishBackup] will only be called when we return [TRANSPORT_OK] here
|
// [finishBackup] will only be called when we return [TRANSPORT_OK] here
|
||||||
// so we remember that we initialized successfully
|
// so we remember that we initialized successfully
|
||||||
|
@ -92,7 +106,6 @@ internal class BackupCoordinator(
|
||||||
if (getBackupBackoff() == 0L) nm.onBackupError()
|
if (getBackupBackoff() == 0L) nm.onBackupError()
|
||||||
TRANSPORT_ERROR
|
TRANSPORT_ERROR
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun isAppEligibleForBackup(
|
fun isAppEligibleForBackup(
|
||||||
targetPackage: PackageInfo,
|
targetPackage: PackageInfo,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.stevesoltys.seedvault.transport.backup
|
package com.stevesoltys.seedvault.transport.backup
|
||||||
|
|
||||||
|
import android.app.backup.RestoreSet
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
@ -11,13 +12,18 @@ interface BackupPlugin {
|
||||||
val fullBackupPlugin: FullBackupPlugin
|
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
|
* This is typically followed by a call to [initializeDevice].
|
||||||
* false if the device was initialized already and initialization should be a no-op.
|
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@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.
|
* Returns an [OutputStream] for writing backup metadata.
|
||||||
|
|
|
@ -112,7 +112,7 @@ internal class KVRestore(
|
||||||
* Return a list of the records (represented by key files) in the given directory,
|
* 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.
|
* sorted lexically by the Base64-decoded key file name, not by the on-disk filename.
|
||||||
*/
|
*/
|
||||||
private fun getSortedKeys(token: Long, packageInfo: PackageInfo): List<DecodedKey>? {
|
private suspend fun getSortedKeys(token: Long, packageInfo: PackageInfo): List<DecodedKey>? {
|
||||||
val records: List<String> = try {
|
val records: List<String> = try {
|
||||||
plugin.listRecords(token, packageInfo)
|
plugin.listRecords(token, packageInfo)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
|
|
|
@ -20,7 +20,7 @@ interface KVRestorePlugin {
|
||||||
* For file-based plugins, this is usually a list of file names in the package directory.
|
* For file-based plugins, this is usually a list of file names in the package directory.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun listRecords(token: Long, packageInfo: PackageInfo): List<String>
|
suspend fun listRecords(token: Long, packageInfo: PackageInfo): List<String>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an [InputStream] for the given token, package and key
|
* Return an [InputStream] for the given token, package and key
|
||||||
|
|
|
@ -13,7 +13,6 @@ import android.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.collection.LongSparseArray
|
import androidx.collection.LongSparseArray
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
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.MetadataManager
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataReader
|
import com.stevesoltys.seedvault.metadata.MetadataReader
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import libcore.io.IoUtils.closeQuietly
|
import libcore.io.IoUtils.closeQuietly
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
private class RestoreCoordinatorState(
|
private class RestoreCoordinatorState(
|
||||||
internal val token: Long,
|
val token: Long,
|
||||||
internal val packages: Iterator<PackageInfo>,
|
val packages: Iterator<PackageInfo>,
|
||||||
/**
|
/**
|
||||||
* Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@
|
* 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
|
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.
|
* or 0 if there is no backup set available corresponding to the current device state.
|
||||||
*/
|
*/
|
||||||
fun getCurrentRestoreSet(): Long {
|
fun getCurrentRestoreSet(): Long {
|
||||||
return metadataManager.getBackupToken()
|
return (settingsManager.getToken() ?: 0L).apply {
|
||||||
.apply { Log.i(TAG, "Got current restore set token: $this") }
|
Log.i(TAG, "Got current restore set token: $this")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -8,36 +8,51 @@ import android.net.Uri
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
|
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
|
||||||
|
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
||||||
import com.stevesoltys.seedvault.transport.requestBackup
|
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
|
private val TAG = BackupStorageViewModel::class.java.simpleName
|
||||||
|
|
||||||
internal class BackupStorageViewModel(
|
internal class BackupStorageViewModel(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val backupManager: IBackupManager,
|
private val backupManager: IBackupManager,
|
||||||
settingsManager: SettingsManager) : StorageViewModel(app, settingsManager) {
|
private val backupCoordinator: BackupCoordinator,
|
||||||
|
settingsManager: SettingsManager
|
||||||
|
) : StorageViewModel(app, settingsManager) {
|
||||||
|
|
||||||
override val isRestoreOperation = false
|
override val isRestoreOperation = false
|
||||||
|
|
||||||
override fun onLocationSet(uri: Uri) {
|
override fun onLocationSet(uri: Uri) {
|
||||||
val isUsb = saveStorage(uri)
|
val isUsb = saveStorage(uri)
|
||||||
settingsManager.forceStorageInitialization()
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
// initialize the new location, will also generate a new backup token
|
// will also generate a new backup token for the new restore set
|
||||||
val observer = InitializationObserver()
|
backupCoordinator.startNewRestoreSet()
|
||||||
backupManager.initializeTransportsForUser(UserHandle.myUserId(), arrayOf(TRANSPORT_ID), observer)
|
|
||||||
|
|
||||||
|
// 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
|
// if storage is on USB and this is not SetupWizard, do a backup right away
|
||||||
if (isUsb && !isSetupWizard) Thread {
|
InitializationObserver(isUsb && !isSetupWizard)
|
||||||
requestBackup(app)
|
)
|
||||||
}.start()
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Error starting new RestoreSet", e)
|
||||||
|
onInitializationError()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private inner class InitializationObserver : IBackupObserver.Stub() {
|
private inner class InitializationObserver(val requestBackup: Boolean) :
|
||||||
|
IBackupObserver.Stub() {
|
||||||
override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) {
|
override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
@ -53,12 +68,19 @@ internal class BackupStorageViewModel(
|
||||||
if (status == 0) {
|
if (status == 0) {
|
||||||
// notify the UI that the location has been set
|
// notify the UI that the location has been set
|
||||||
mLocationChecked.postEvent(LocationResult())
|
mLocationChecked.postEvent(LocationResult())
|
||||||
|
if (requestBackup) {
|
||||||
|
requestBackup(app)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// notify the UI that the location was invalid
|
// notify the UI that the location was invalid
|
||||||
val errorMsg = app.getString(R.string.storage_check_fragment_backup_error)
|
onInitializationError()
|
||||||
mLocationChecked.postEvent(LocationResult(errorMsg))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onInitializationError() {
|
||||||
|
val errorMsg = app.getString(R.string.storage_check_fragment_backup_error)
|
||||||
|
mLocationChecked.postEvent(LocationResult(errorMsg))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<DocumentsStorage>()
|
||||||
|
private val kvBackupPlugin: KVBackupPlugin = mockk<DocumentsProviderKVBackup>()
|
||||||
|
private val fullBackupPlugin: FullBackupPlugin = mockk<DocumentsProviderFullBackup>()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||||
import android.app.backup.RestoreDescription
|
import android.app.backup.RestoreDescription
|
||||||
import android.app.backup.RestoreDescription.TYPE_FULL_STREAM
|
import android.app.backup.RestoreDescription.TYPE_FULL_STREAM
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
|
||||||
import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
|
import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
|
||||||
import com.stevesoltys.seedvault.crypto.CryptoImpl
|
import com.stevesoltys.seedvault.crypto.CryptoImpl
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
|
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.OutputFactory
|
||||||
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
|
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
|
||||||
import com.stevesoltys.seedvault.transport.restore.RestorePlugin
|
import com.stevesoltys.seedvault.transport.restore.RestorePlugin
|
||||||
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import io.mockk.CapturingSlot
|
import io.mockk.CapturingSlot
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
|
@ -186,7 +186,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
val backupDataOutput = mockk<BackupDataOutput>()
|
val backupDataOutput = mockk<BackupDataOutput>()
|
||||||
val rInputStream = ByteArrayInputStream(bOutputStream.toByteArray())
|
val rInputStream = ByteArrayInputStream(bOutputStream.toByteArray())
|
||||||
val rInputStream2 = ByteArrayInputStream(bOutputStream2.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
|
every { outputFactory.getBackupDataOutput(fileDescriptor) } returns backupDataOutput
|
||||||
coEvery {
|
coEvery {
|
||||||
kvRestorePlugin.getInputStreamForRecord(
|
kvRestorePlugin.getInputStreamForRecord(
|
||||||
|
@ -255,7 +255,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
// restore finds the backed up key and writes the decrypted value
|
// restore finds the backed up key and writes the decrypted value
|
||||||
val backupDataOutput = mockk<BackupDataOutput>()
|
val backupDataOutput = mockk<BackupDataOutput>()
|
||||||
val rInputStream = ByteArrayInputStream(bOutputStream.toByteArray())
|
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
|
every { outputFactory.getBackupDataOutput(fileDescriptor) } returns backupDataOutput
|
||||||
coEvery {
|
coEvery {
|
||||||
kvRestorePlugin.getInputStreamForRecord(
|
kvRestorePlugin.getInputStreamForRecord(
|
||||||
|
|
|
@ -8,7 +8,6 @@ import android.content.pm.PackageInfo
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.coAssertThrows
|
import com.stevesoltys.seedvault.coAssertThrows
|
||||||
import com.stevesoltys.seedvault.getRandomString
|
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.QUOTA_EXCEEDED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||||
import com.stevesoltys.seedvault.settings.Storage
|
import com.stevesoltys.seedvault.settings.Storage
|
||||||
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
|
@ -63,9 +63,18 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
private val storage = Storage(Uri.EMPTY, getRandomString(), false)
|
private val storage = Storage(Uri.EMPTY, getRandomString(), false)
|
||||||
|
|
||||||
@Test
|
@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
|
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
|
coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||||
every { metadataManager.onDeviceInitialization(token, metadataOutputStream) } just Runs
|
every { metadataManager.onDeviceInitialization(token, metadataOutputStream) } just Runs
|
||||||
every { kv.hasState() } returns false
|
every { kv.hasState() } returns false
|
||||||
|
@ -79,9 +88,8 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `device initialization does no-op when already initialized`() = runBlocking {
|
fun `device initialization does no-op when no token available`() = runBlocking {
|
||||||
every { clock.time() } returns token
|
every { settingsManager.getToken() } returns null
|
||||||
coEvery { plugin.initializeDevice(token) } returns false
|
|
||||||
every { kv.hasState() } returns false
|
every { kv.hasState() } returns false
|
||||||
every { full.hasState() } returns false
|
every { full.hasState() } returns false
|
||||||
|
|
||||||
|
@ -91,8 +99,8 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `error notification when device initialization fails`() = runBlocking {
|
fun `error notification when device initialization fails`() = runBlocking {
|
||||||
every { clock.time() } returns token
|
every { settingsManager.getToken() } returns token
|
||||||
coEvery { plugin.initializeDevice(token) } throws IOException()
|
coEvery { plugin.initializeDevice() } throws IOException()
|
||||||
every { settingsManager.getStorage() } returns storage
|
every { settingsManager.getStorage() } returns storage
|
||||||
every { notificationManager.onBackupError() } just Runs
|
every { notificationManager.onBackupError() } just Runs
|
||||||
|
|
||||||
|
@ -112,8 +120,8 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
val storage = mockk<Storage>()
|
val storage = mockk<Storage>()
|
||||||
val documentFile = mockk<DocumentFile>()
|
val documentFile = mockk<DocumentFile>()
|
||||||
|
|
||||||
every { clock.time() } returns token
|
every { settingsManager.getToken() } returns token
|
||||||
coEvery { plugin.initializeDevice(token) } throws IOException()
|
coEvery { plugin.initializeDevice() } throws IOException()
|
||||||
every { settingsManager.getStorage() } returns storage
|
every { settingsManager.getStorage() } returns storage
|
||||||
every { storage.isUsb } returns true
|
every { storage.isUsb } returns true
|
||||||
every { storage.getDocumentFile(context) } returns documentFile
|
every { storage.getDocumentFile(context) } returns documentFile
|
||||||
|
|
|
@ -56,7 +56,7 @@ internal class KVRestoreTest : RestoreTest() {
|
||||||
fun `listing records throws`() = runBlocking {
|
fun `listing records throws`() = runBlocking {
|
||||||
restore.initializeState(token, packageInfo)
|
restore.initializeState(token, packageInfo)
|
||||||
|
|
||||||
every { plugin.listRecords(token, packageInfo) } throws IOException()
|
coEvery { plugin.listRecords(token, packageInfo) } throws IOException()
|
||||||
|
|
||||||
assertEquals(TRANSPORT_ERROR, restore.getRestoreData(fileDescriptor))
|
assertEquals(TRANSPORT_ERROR, restore.getRestoreData(fileDescriptor))
|
||||||
}
|
}
|
||||||
|
@ -208,7 +208,7 @@ internal class KVRestoreTest : RestoreTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRecordsAndOutput(recordKeys: List<String> = listOf(key64)) {
|
private fun getRecordsAndOutput(recordKeys: List<String> = listOf(key64)) {
|
||||||
every { plugin.listRecords(token, packageInfo) } returns recordKeys
|
coEvery { plugin.listRecords(token, packageInfo) } returns recordKeys
|
||||||
every { outputFactory.getBackupDataOutput(fileDescriptor) } returns output
|
every { outputFactory.getBackupDataOutput(fileDescriptor) } returns output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ import android.app.backup.RestoreDescription.TYPE_KEY_VALUE
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
|
||||||
import com.stevesoltys.seedvault.coAssertThrows
|
import com.stevesoltys.seedvault.coAssertThrows
|
||||||
import com.stevesoltys.seedvault.getRandomString
|
import com.stevesoltys.seedvault.getRandomString
|
||||||
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
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.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.settings.Storage
|
import com.stevesoltys.seedvault.settings.Storage
|
||||||
import com.stevesoltys.seedvault.transport.TransportTest
|
import com.stevesoltys.seedvault.transport.TransportTest
|
||||||
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
@ -91,7 +91,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getCurrentRestoreSet() delegates to plugin`() {
|
fun `getCurrentRestoreSet() delegates to plugin`() {
|
||||||
every { metadataManager.getBackupToken() } returns token
|
every { settingsManager.getToken() } returns token
|
||||||
assertEquals(token, restore.getCurrentRestoreSet())
|
assertEquals(token, restore.getCurrentRestoreSet())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue