Introduce StoragePluginManager to handle storage plugins
and allow changing them dynamically. So far plugins were injected into the dependency graph and couldn't be changed at runtime, only their config could. Now we have the infrastructure in place to really allow for more than one plugin.
This commit is contained in:
parent
2489190824
commit
7e612cb8e0
70 changed files with 720 additions and 502 deletions
|
@ -41,19 +41,20 @@ class KoinInstrumentationTestApp : App() {
|
||||||
|
|
||||||
viewModel {
|
viewModel {
|
||||||
currentRestoreViewModel =
|
currentRestoreViewModel =
|
||||||
spyk(RestoreViewModel(context, get(), get(), get(), get(), get(), get()))
|
spyk(RestoreViewModel(context, get(), get(), get(), get(), get(), get(), get()))
|
||||||
currentRestoreViewModel!!
|
currentRestoreViewModel!!
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel {
|
viewModel {
|
||||||
currentBackupStorageViewModel =
|
val viewModel =
|
||||||
spyk(BackupStorageViewModel(context, get(), get(), get(), get()))
|
BackupStorageViewModel(context, get(), get(), get(), get(), get(), get(), get())
|
||||||
|
currentBackupStorageViewModel = spyk(viewModel)
|
||||||
currentBackupStorageViewModel!!
|
currentBackupStorageViewModel!!
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel {
|
viewModel {
|
||||||
currentRestoreStorageViewModel =
|
currentRestoreStorageViewModel =
|
||||||
spyk(RestoreStorageViewModel(context, get(), get()))
|
spyk(RestoreStorageViewModel(context, get(), get(), get(), get()))
|
||||||
currentRestoreStorageViewModel!!
|
currentRestoreStorageViewModel!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.stevesoltys.seedvault
|
package com.stevesoltys.seedvault
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
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.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
|
@ -28,20 +29,24 @@ import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
@MediumTest
|
@MediumTest
|
||||||
class PluginTest : KoinComponent {
|
class PluginTest : KoinComponent {
|
||||||
|
|
||||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
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, mockedSettingsManager)
|
private val storage = DocumentsStorage(
|
||||||
|
appContext = context,
|
||||||
|
settingsManager = mockedSettingsManager,
|
||||||
|
safStorage = settingsManager.getSafStorage() ?: error("No SAF storage"),
|
||||||
|
)
|
||||||
|
|
||||||
private val storagePlugin: StoragePlugin = DocumentsProviderStoragePlugin(context, storage)
|
private val storagePlugin: StoragePlugin<Uri> = DocumentsProviderStoragePlugin(context, storage)
|
||||||
|
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
private val legacyStoragePlugin: LegacyStoragePlugin =
|
private val legacyStoragePlugin: LegacyStoragePlugin = DocumentsProviderLegacyPlugin(context) {
|
||||||
DocumentsProviderLegacyPlugin(context, storage)
|
storage
|
||||||
|
}
|
||||||
|
|
||||||
private val token = System.currentTimeMillis() - 365L * 24L * 60L * 60L * 1000L
|
private val token = System.currentTimeMillis() - 365L * 24L * 60L * 60L * 1000L
|
||||||
private val packageInfo = PackageInfoBuilder.newBuilder().setPackageName("org.example").build()
|
private val packageInfo = PackageInfoBuilder.newBuilder().setPackageName("org.example").build()
|
||||||
|
@ -76,8 +81,6 @@ class PluginTest : KoinComponent {
|
||||||
fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) {
|
fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) {
|
||||||
// no backups available initially
|
// no backups available initially
|
||||||
assertEquals(0, storagePlugin.getAvailableBackups()?.toList()?.size)
|
assertEquals(0, storagePlugin.getAvailableBackups()?.toList()?.size)
|
||||||
val s = settingsManager.getSafStorage() ?: error("no storage")
|
|
||||||
assertFalse(storagePlugin.hasBackup(s))
|
|
||||||
|
|
||||||
// prepare returned tokens requested when initializing device
|
// prepare returned tokens requested when initializing device
|
||||||
every { mockedSettingsManager.getToken() } returnsMany listOf(token, token + 1, token + 1)
|
every { mockedSettingsManager.getToken() } returnsMany listOf(token, token + 1, token + 1)
|
||||||
|
@ -92,7 +95,6 @@ class PluginTest : KoinComponent {
|
||||||
|
|
||||||
// one backup available now
|
// one backup available now
|
||||||
assertEquals(1, storagePlugin.getAvailableBackups()?.toList()?.size)
|
assertEquals(1, storagePlugin.getAvailableBackups()?.toList()?.size)
|
||||||
assertTrue(storagePlugin.hasBackup(s))
|
|
||||||
|
|
||||||
// initializing again (with another restore set) does add a restore set
|
// initializing again (with another restore set) does add a restore set
|
||||||
storagePlugin.startNewRestoreSet(token + 1)
|
storagePlugin.startNewRestoreSet(token + 1)
|
||||||
|
@ -100,7 +102,6 @@ class PluginTest : KoinComponent {
|
||||||
storagePlugin.getOutputStream(token + 1, FILE_BACKUP_METADATA)
|
storagePlugin.getOutputStream(token + 1, FILE_BACKUP_METADATA)
|
||||||
.writeAndClose(getRandomByteArray())
|
.writeAndClose(getRandomByteArray())
|
||||||
assertEquals(2, storagePlugin.getAvailableBackups()?.toList()?.size)
|
assertEquals(2, storagePlugin.getAvailableBackups()?.toList()?.size)
|
||||||
assertTrue(storagePlugin.hasBackup(s))
|
|
||||||
|
|
||||||
// initializing again (without new restore set) doesn't change number of restore sets
|
// initializing again (without new restore set) doesn't change number of restore sets
|
||||||
storagePlugin.initializeDevice()
|
storagePlugin.initializeDevice()
|
||||||
|
|
|
@ -39,13 +39,16 @@ import java.io.IOException
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
@MediumTest
|
@MediumTest
|
||||||
class DocumentsStorageTest : KoinComponent {
|
class DocumentsStorageTest : KoinComponent {
|
||||||
|
|
||||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
private val settingsManager by inject<SettingsManager>()
|
private val settingsManager by inject<SettingsManager>()
|
||||||
private val storage = DocumentsStorage(context, settingsManager)
|
private val storage = DocumentsStorage(
|
||||||
|
appContext = context,
|
||||||
|
settingsManager = settingsManager,
|
||||||
|
safStorage = settingsManager.getSafStorage() ?: error("No SAF storage"),
|
||||||
|
)
|
||||||
|
|
||||||
private val filename = getRandomBase64()
|
private val filename = getRandomBase64()
|
||||||
private lateinit var file: DocumentFile
|
private lateinit var file: DocumentFile
|
||||||
|
|
|
@ -24,7 +24,7 @@ class PackageServiceTest : KoinComponent {
|
||||||
|
|
||||||
private val settingsManager: SettingsManager by inject()
|
private val settingsManager: SettingsManager by inject()
|
||||||
|
|
||||||
private val storagePlugin: StoragePlugin by inject()
|
private val storagePlugin: StoragePlugin<*> by inject()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testNotAllowedPackages() {
|
fun testNotAllowedPackages() {
|
||||||
|
|
|
@ -13,13 +13,14 @@ import android.os.StrictMode
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.os.UserManager
|
import android.os.UserManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.work.WorkManager
|
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
|
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
|
||||||
|
import androidx.work.WorkManager
|
||||||
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.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.StoragePluginManager
|
||||||
|
import com.stevesoltys.seedvault.plugins.saf.storagePluginModuleSaf
|
||||||
import com.stevesoltys.seedvault.restore.RestoreViewModel
|
import com.stevesoltys.seedvault.restore.RestoreViewModel
|
||||||
import com.stevesoltys.seedvault.restore.install.installModule
|
import com.stevesoltys.seedvault.restore.install.installModule
|
||||||
import com.stevesoltys.seedvault.settings.AppListRetriever
|
import com.stevesoltys.seedvault.settings.AppListRetriever
|
||||||
|
@ -54,15 +55,41 @@ open class App : Application() {
|
||||||
private val appModule = module {
|
private val appModule = module {
|
||||||
single { SettingsManager(this@App) }
|
single { SettingsManager(this@App) }
|
||||||
single { BackupNotificationManager(this@App) }
|
single { BackupNotificationManager(this@App) }
|
||||||
|
single { StoragePluginManager(this@App, get(), get()) }
|
||||||
single { Clock() }
|
single { Clock() }
|
||||||
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
|
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
|
||||||
factory { AppListRetriever(this@App, get(), get(), get()) }
|
factory { AppListRetriever(this@App, get(), get(), get()) }
|
||||||
|
|
||||||
viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get(), get()) }
|
viewModel {
|
||||||
|
SettingsViewModel(
|
||||||
|
app = this@App,
|
||||||
|
settingsManager = get(),
|
||||||
|
keyManager = get(),
|
||||||
|
pluginManager = get(),
|
||||||
|
metadataManager = get(),
|
||||||
|
appListRetriever = get(),
|
||||||
|
storageBackup = get(),
|
||||||
|
backupManager = get(),
|
||||||
|
backupInitializer = get(),
|
||||||
|
)
|
||||||
|
}
|
||||||
viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
||||||
viewModel { BackupStorageViewModel(this@App, get(), get(), get(), get()) }
|
viewModel { BackupStorageViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
||||||
viewModel { RestoreStorageViewModel(this@App, get(), get()) }
|
viewModel { RestoreStorageViewModel(this@App, get(), get(), get()) }
|
||||||
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get(), get()) }
|
||||||
|
viewModel {
|
||||||
|
BackupStorageViewModel(
|
||||||
|
app = this@App,
|
||||||
|
backupManager = get(),
|
||||||
|
backupInitializer = get(),
|
||||||
|
storageBackup = get(),
|
||||||
|
safHandler = get(),
|
||||||
|
settingsManager = get(),
|
||||||
|
storagePluginManager = get(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewModel { RestoreStorageViewModel(this@App, get(), get(), get()) }
|
||||||
|
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get(), get()) }
|
||||||
viewModel { FileSelectionViewModel(this@App, get()) }
|
viewModel { FileSelectionViewModel(this@App, get()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +127,7 @@ open class App : Application() {
|
||||||
cryptoModule,
|
cryptoModule,
|
||||||
headerModule,
|
headerModule,
|
||||||
metadataModule,
|
metadataModule,
|
||||||
documentsProviderModule, // storage plugin
|
storagePluginModuleSaf, // storage plugin
|
||||||
backupModule,
|
backupModule,
|
||||||
restoreModule,
|
restoreModule,
|
||||||
installModule,
|
installModule,
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
package com.stevesoltys.seedvault.plugins
|
package com.stevesoltys.seedvault.plugins
|
||||||
|
|
||||||
import android.app.backup.RestoreSet
|
import android.app.backup.RestoreSet
|
||||||
import androidx.annotation.WorkerThread
|
|
||||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
interface StoragePlugin {
|
interface StoragePlugin<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the plugin is working, or false if it isn't.
|
* Returns true if the plugin is working, or false if it isn't.
|
||||||
|
@ -53,14 +51,6 @@ interface StoragePlugin {
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
suspend fun removeData(token: Long, name: String)
|
suspend fun removeData(token: Long, name: String)
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches if there's really a backup available in the given storage location.
|
|
||||||
* Returns true if at least one was found and false otherwise.
|
|
||||||
*/
|
|
||||||
@WorkerThread
|
|
||||||
@Throws(IOException::class)
|
|
||||||
suspend fun hasBackup(safStorage: SafStorage): Boolean
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the set of all backups currently available for restore.
|
* Get the set of all backups currently available for restore.
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.stevesoltys.seedvault.plugins
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import com.stevesoltys.seedvault.getStorageContext
|
||||||
|
import com.stevesoltys.seedvault.permitDiskReads
|
||||||
|
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderStoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
|
||||||
|
import com.stevesoltys.seedvault.plugins.saf.SafFactory
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
|
import com.stevesoltys.seedvault.settings.StoragePluginEnum
|
||||||
|
|
||||||
|
abstract class StorageProperties<T> {
|
||||||
|
abstract val config: T
|
||||||
|
abstract val name: String
|
||||||
|
abstract val isUsb: Boolean
|
||||||
|
abstract val requiresNetwork: Boolean
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
abstract fun isUnavailableUsb(context: Context): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this is storage that requires network access,
|
||||||
|
* but it isn't available right now.
|
||||||
|
*/
|
||||||
|
fun isUnavailableNetwork(context: Context, allowMetered: Boolean): Boolean {
|
||||||
|
return requiresNetwork && !hasUnmeteredInternet(context, allowMetered)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasUnmeteredInternet(context: Context, allowMetered: Boolean): Boolean {
|
||||||
|
val cm = context.getSystemService(ConnectivityManager::class.java) ?: return false
|
||||||
|
val isMetered = cm.isActiveNetworkMetered
|
||||||
|
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
|
||||||
|
return capabilities.hasCapability(NET_CAPABILITY_INTERNET) &&
|
||||||
|
(allowMetered || !isMetered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StoragePluginManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val settingsManager: SettingsManager,
|
||||||
|
safFactory: SafFactory,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var _appPlugin: StoragePlugin<*>?
|
||||||
|
private var _filesPlugin: org.calyxos.backup.storage.api.StoragePlugin?
|
||||||
|
private var _storageProperties: StorageProperties<*>?
|
||||||
|
|
||||||
|
val appPlugin: StoragePlugin<*>
|
||||||
|
@Synchronized
|
||||||
|
get() {
|
||||||
|
return _appPlugin ?: error("App plugin was loaded, but still null")
|
||||||
|
}
|
||||||
|
|
||||||
|
val filesPlugin: org.calyxos.backup.storage.api.StoragePlugin
|
||||||
|
@Synchronized
|
||||||
|
get() {
|
||||||
|
return _filesPlugin ?: error("Files plugin was loaded, but still null")
|
||||||
|
}
|
||||||
|
|
||||||
|
val storageProperties: StorageProperties<*>?
|
||||||
|
@Synchronized
|
||||||
|
get() {
|
||||||
|
return _storageProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
when (settingsManager.storagePluginType) {
|
||||||
|
StoragePluginEnum.SAF -> {
|
||||||
|
val safStorage = settingsManager.getSafStorage() ?: error("No SAF storage saved")
|
||||||
|
val documentsStorage = DocumentsStorage(context, settingsManager, safStorage)
|
||||||
|
_appPlugin = safFactory.createAppStoragePlugin(safStorage, documentsStorage)
|
||||||
|
_filesPlugin = safFactory.createFilesStoragePlugin(safStorage, documentsStorage)
|
||||||
|
_storageProperties = safStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> {
|
||||||
|
_appPlugin = null
|
||||||
|
_filesPlugin = null
|
||||||
|
_storageProperties = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isValidAppPluginSet(): Boolean {
|
||||||
|
if (_appPlugin == null || _filesPlugin == null) return false
|
||||||
|
if (_appPlugin is DocumentsProviderStoragePlugin) {
|
||||||
|
val storage = settingsManager.getSafStorage() ?: return false
|
||||||
|
if (storage.isUsb) return true
|
||||||
|
return permitDiskReads {
|
||||||
|
storage.getDocumentFile(context).isDirectory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the storage plugins and current [StorageProperties].
|
||||||
|
*
|
||||||
|
* IMPORTANT: Do no call this while current plugins are being used,
|
||||||
|
* e.g. while backup/restore operation is still running.
|
||||||
|
*/
|
||||||
|
fun <T> changePlugins(
|
||||||
|
storageProperties: StorageProperties<T>,
|
||||||
|
appPlugin: StoragePlugin<T>,
|
||||||
|
filesPlugin: org.calyxos.backup.storage.api.StoragePlugin,
|
||||||
|
) {
|
||||||
|
settingsManager.setStoragePlugin(appPlugin)
|
||||||
|
_storageProperties = storageProperties
|
||||||
|
_appPlugin = appPlugin
|
||||||
|
_filesPlugin = filesPlugin
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we are able to do backups now by examining possible pre-conditions
|
||||||
|
* such as plugged-in flash drive or internet access.
|
||||||
|
*
|
||||||
|
* Should be run off the UI thread (ideally I/O) because of disk access.
|
||||||
|
*
|
||||||
|
* @return true if a backup is possible, false if not.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
fun canDoBackupNow(): Boolean {
|
||||||
|
val storage = storageProperties ?: return false
|
||||||
|
val systemContext = context.getStorageContext { storage.isUsb }
|
||||||
|
return !storage.isUnavailableUsb(systemContext) &&
|
||||||
|
!storage.isUnavailableNetwork(context, settingsManager.useMeteredNetwork)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -10,12 +10,13 @@ import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@Suppress("BlockingMethodInNonBlockingContext", "Deprecation") // all methods do I/O
|
@Suppress("Deprecation")
|
||||||
internal class DocumentsProviderLegacyPlugin(
|
internal class DocumentsProviderLegacyPlugin(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val storage: DocumentsStorage,
|
private val storageGetter: () -> DocumentsStorage,
|
||||||
) : LegacyStoragePlugin {
|
) : LegacyStoragePlugin {
|
||||||
|
|
||||||
|
private val storage get() = storageGetter()
|
||||||
private var packageDir: DocumentFile? = null
|
private var packageDir: DocumentFile? = null
|
||||||
private var packageChildren: List<DocumentFile>? = null
|
private var packageChildren: List<DocumentFile>? = null
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
package com.stevesoltys.seedvault.plugins.saf
|
package com.stevesoltys.seedvault.plugins.saf
|
||||||
|
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
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 storagePluginModuleSaf = module {
|
||||||
single { DocumentsStorage(androidContext(), get()) }
|
single { SafFactory(androidContext(), get(), get()) }
|
||||||
|
single { SafHandler(androidContext(), get(), get(), get()) }
|
||||||
|
|
||||||
single<StoragePlugin> { DocumentsProviderStoragePlugin(androidContext(), get()) }
|
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
single<LegacyStoragePlugin> { DocumentsProviderLegacyPlugin(androidContext(), get()) }
|
single<LegacyStoragePlugin> {
|
||||||
|
DocumentsProviderLegacyPlugin(
|
||||||
|
context = androidContext(),
|
||||||
|
storageGetter = {
|
||||||
|
val safStorage = get<SettingsManager>().getSafStorage() ?: error("No SAF storage")
|
||||||
|
DocumentsStorage(androidContext(), get(), safStorage)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.plugins.saf
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.stevesoltys.seedvault.getStorageContext
|
import com.stevesoltys.seedvault.getStorageContext
|
||||||
|
@ -16,19 +17,15 @@ import java.io.OutputStream
|
||||||
|
|
||||||
private val TAG = DocumentsProviderStoragePlugin::class.java.simpleName
|
private val TAG = DocumentsProviderStoragePlugin::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class DocumentsProviderStoragePlugin(
|
internal class DocumentsProviderStoragePlugin(
|
||||||
private val appContext: Context,
|
private val appContext: Context,
|
||||||
private val storage: DocumentsStorage,
|
private val storage: DocumentsStorage,
|
||||||
) : StoragePlugin {
|
) : StoragePlugin<Uri> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attention: This context might be from a different user. Use with care.
|
* Attention: This context might be from a different user. Use with care.
|
||||||
*/
|
*/
|
||||||
private val context: Context
|
private val context: Context get() = appContext.getStorageContext { storage.safStorage.isUsb }
|
||||||
get() = appContext.getStorageContext {
|
|
||||||
storage.safStorage?.isUsb == true
|
|
||||||
}
|
|
||||||
|
|
||||||
private val packageManager: PackageManager = appContext.packageManager
|
private val packageManager: PackageManager = appContext.packageManager
|
||||||
|
|
||||||
|
@ -77,16 +74,6 @@ internal class DocumentsProviderStoragePlugin(
|
||||||
if (!file.delete()) throw IOException("Failed to delete $name")
|
if (!file.delete()) throw IOException("Failed to delete $name")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override suspend fun hasBackup(safStorage: SafStorage): Boolean {
|
|
||||||
// potentially get system user context if needed here
|
|
||||||
val c = appContext.getStorageContext { safStorage.isUsb }
|
|
||||||
val parent = DocumentFile.fromTreeUri(c, safStorage.uri) ?: throw AssertionError()
|
|
||||||
val rootDir = parent.findFileBlocking(c, DIRECTORY_ROOT) ?: return false
|
|
||||||
val backupSets = getBackups(c, rootDir)
|
|
||||||
return backupSets.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getAvailableBackups(): Sequence<EncryptedMetadata>? {
|
override suspend fun getAvailableBackups(): Sequence<EncryptedMetadata>? {
|
||||||
val rootDir = storage.rootBackupDir ?: return null
|
val rootDir = storage.rootBackupDir ?: return null
|
||||||
val backupSets = getBackups(context, rootDir)
|
val backupSets = getBackups(context, rootDir)
|
||||||
|
@ -110,7 +97,6 @@ internal class DocumentsProviderStoragePlugin(
|
||||||
|
|
||||||
class BackupSet(val token: Long, val metadataFile: DocumentFile)
|
class BackupSet(val token: Long, val metadataFile: DocumentFile)
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal suspend fun getBackups(context: Context, rootDir: DocumentFile): List<BackupSet> {
|
internal suspend fun getBackups(context: Context, rootDir: DocumentFile): List<BackupSet> {
|
||||||
val backupSets = ArrayList<BackupSet>()
|
val backupSets = ArrayList<BackupSet>()
|
||||||
val files = try {
|
val files = try {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
@file:Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
|
|
||||||
package com.stevesoltys.seedvault.plugins.saf
|
package com.stevesoltys.seedvault.plugins.saf
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
|
@ -43,27 +41,19 @@ private val TAG = DocumentsStorage::class.java.simpleName
|
||||||
internal class DocumentsStorage(
|
internal class DocumentsStorage(
|
||||||
private val appContext: Context,
|
private val appContext: Context,
|
||||||
private val settingsManager: SettingsManager,
|
private val settingsManager: SettingsManager,
|
||||||
|
internal val safStorage: SafStorage,
|
||||||
) {
|
) {
|
||||||
internal var safStorage: SafStorage? = null
|
|
||||||
get() {
|
|
||||||
if (field == null) field = settingsManager.getSafStorage()
|
|
||||||
return field
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attention: This context might be from a different user. Use with care.
|
* Attention: This context might be from a different user. Use with care.
|
||||||
*/
|
*/
|
||||||
private val context: Context
|
private val context: Context get() = appContext.getStorageContext { safStorage.isUsb }
|
||||||
get() = appContext.getStorageContext {
|
|
||||||
safStorage?.isUsb == true
|
|
||||||
}
|
|
||||||
private val contentResolver: ContentResolver get() = context.contentResolver
|
private val contentResolver: ContentResolver get() = context.contentResolver
|
||||||
|
|
||||||
internal var rootBackupDir: DocumentFile? = null
|
internal var rootBackupDir: DocumentFile? = null
|
||||||
get() = runBlocking {
|
get() = runBlocking {
|
||||||
if (field == null) {
|
if (field == null) {
|
||||||
val parent = safStorage?.getDocumentFile(context)
|
val parent = safStorage.getDocumentFile(context)
|
||||||
?: return@runBlocking null
|
|
||||||
field = try {
|
field = try {
|
||||||
parent.createOrGetDirectory(context, DIRECTORY_ROOT).apply {
|
parent.createOrGetDirectory(context, DIRECTORY_ROOT).apply {
|
||||||
// create .nomedia file to prevent Android's MediaScanner
|
// create .nomedia file to prevent Android's MediaScanner
|
||||||
|
@ -103,13 +93,12 @@ internal class DocumentsStorage(
|
||||||
* Resets this storage abstraction, forcing it to re-fetch cached values on next access.
|
* Resets this storage abstraction, forcing it to re-fetch cached values on next access.
|
||||||
*/
|
*/
|
||||||
fun reset(newToken: Long?) {
|
fun reset(newToken: Long?) {
|
||||||
safStorage = null
|
|
||||||
currentToken = newToken
|
currentToken = newToken
|
||||||
rootBackupDir = null
|
rootBackupDir = null
|
||||||
currentSetDir = null
|
currentSetDir = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAuthority(): String? = safStorage?.uri?.authority
|
fun getAuthority(): String? = safStorage.uri.authority
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
suspend fun getSetDir(token: Long = currentToken ?: error("no token")): DocumentFile? {
|
suspend fun getSetDir(token: Long = currentToken ?: error("no token")): DocumentFile? {
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.stevesoltys.seedvault.plugins.saf
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
|
import com.stevesoltys.seedvault.storage.SeedvaultSafStoragePlugin
|
||||||
|
|
||||||
|
class SafFactory(
|
||||||
|
private val context: Context,
|
||||||
|
private val keyManager: KeyManager,
|
||||||
|
private val settingsManager: SettingsManager,
|
||||||
|
) {
|
||||||
|
|
||||||
|
internal fun createAppStoragePlugin(
|
||||||
|
safStorage: SafStorage,
|
||||||
|
documentsStorage: DocumentsStorage = DocumentsStorage(context, settingsManager, safStorage),
|
||||||
|
): StoragePlugin<Uri> {
|
||||||
|
return DocumentsProviderStoragePlugin(context, documentsStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun createFilesStoragePlugin(
|
||||||
|
safStorage: SafStorage,
|
||||||
|
documentsStorage: DocumentsStorage = DocumentsStorage(context, settingsManager, safStorage),
|
||||||
|
): org.calyxos.backup.storage.api.StoragePlugin {
|
||||||
|
return SeedvaultSafStoragePlugin(context, documentsStorage, keyManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.stevesoltys.seedvault.plugins.saf
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Context.USB_SERVICE
|
||||||
|
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
import android.hardware.usb.UsbManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import com.stevesoltys.seedvault.R
|
||||||
|
import com.stevesoltys.seedvault.isMassStorage
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
|
import com.stevesoltys.seedvault.settings.FlashDrive
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
|
import com.stevesoltys.seedvault.ui.storage.StorageOption
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
private const val TAG = "SafHandler"
|
||||||
|
|
||||||
|
internal class SafHandler(
|
||||||
|
private val context: Context,
|
||||||
|
private val safFactory: SafFactory,
|
||||||
|
private val settingsManager: SettingsManager,
|
||||||
|
private val storagePluginManager: StoragePluginManager,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun onConfigReceived(uri: Uri, safOption: StorageOption.SafOption): SafStorage {
|
||||||
|
// persist permission to access backup folder across reboots
|
||||||
|
val takeFlags = FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||||
|
|
||||||
|
val name = if (safOption.isInternal()) {
|
||||||
|
"${safOption.title} (${context.getString(R.string.settings_backup_location_internal)})"
|
||||||
|
} else {
|
||||||
|
safOption.title
|
||||||
|
}
|
||||||
|
return SafStorage(uri, name, safOption.isUsb, safOption.requiresNetwork)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches if there's really an app backup available in the given storage location.
|
||||||
|
* Returns true if at least one was found and false otherwise.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
@Throws(IOException::class)
|
||||||
|
suspend fun hasAppBackup(safStorage: SafStorage): Boolean {
|
||||||
|
val storage = DocumentsStorage(context, settingsManager, safStorage)
|
||||||
|
val appPlugin = safFactory.createAppStoragePlugin(safStorage, storage)
|
||||||
|
val backups = appPlugin.getAvailableBackups()
|
||||||
|
return backups != null && backups.iterator().hasNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(safStorage: SafStorage) {
|
||||||
|
settingsManager.setSafStorage(safStorage)
|
||||||
|
|
||||||
|
if (safStorage.isUsb) {
|
||||||
|
Log.d(TAG, "Selected storage is a removable USB device.")
|
||||||
|
val wasSaved = saveUsbDevice()
|
||||||
|
// reset stored flash drive, if we did not update it
|
||||||
|
if (!wasSaved) settingsManager.setFlashDrive(null)
|
||||||
|
} else {
|
||||||
|
settingsManager.setFlashDrive(null)
|
||||||
|
}
|
||||||
|
Log.d(TAG, "New storage location saved: ${safStorage.uri}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveUsbDevice(): Boolean {
|
||||||
|
val manager = context.getSystemService(USB_SERVICE) as UsbManager
|
||||||
|
manager.deviceList.values.forEach { device ->
|
||||||
|
if (device.isMassStorage()) {
|
||||||
|
val flashDrive = FlashDrive.from(device)
|
||||||
|
settingsManager.setFlashDrive(flashDrive)
|
||||||
|
Log.d(TAG, "Saved flash drive: $flashDrive")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.e(TAG, "No USB device found even though we were expecting one.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPlugin(safStorage: SafStorage) {
|
||||||
|
val storage = DocumentsStorage(context, settingsManager, safStorage)
|
||||||
|
storagePluginManager.changePlugins(
|
||||||
|
storageProperties = safStorage,
|
||||||
|
appPlugin = safFactory.createAppStoragePlugin(safStorage, storage),
|
||||||
|
filesPlugin = safFactory.createFilesStoragePlugin(safStorage, storage),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,19 +6,21 @@
|
||||||
package com.stevesoltys.seedvault.plugins.saf
|
package com.stevesoltys.seedvault.plugins.saf
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.stevesoltys.seedvault.plugins.StorageProperties
|
||||||
|
|
||||||
data class SafStorage(
|
data class SafStorage(
|
||||||
val uri: Uri,
|
override val config: Uri,
|
||||||
val name: String,
|
override val name: String,
|
||||||
val isUsb: Boolean,
|
override val isUsb: Boolean,
|
||||||
val requiresNetwork: Boolean,
|
override val requiresNetwork: Boolean,
|
||||||
) {
|
) : StorageProperties<Uri>() {
|
||||||
fun getDocumentFile(context: Context) = DocumentFile.fromTreeUri(context, uri)
|
|
||||||
|
val uri: Uri = config
|
||||||
|
|
||||||
|
fun getDocumentFile(context: Context) = DocumentFile.fromTreeUri(context, config)
|
||||||
?: throw AssertionError("Should only happen on API < 21.")
|
?: throw AssertionError("Should only happen on API < 21.")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,23 +29,7 @@ data class SafStorage(
|
||||||
* Must be run off UI thread (ideally I/O).
|
* Must be run off UI thread (ideally I/O).
|
||||||
*/
|
*/
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun isUnavailableUsb(context: Context): Boolean {
|
override fun isUnavailableUsb(context: Context): Boolean {
|
||||||
return isUsb && !getDocumentFile(context).isDirectory
|
return isUsb && !getDocumentFile(context).isDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if this is storage that requires network access,
|
|
||||||
* but it isn't available right now.
|
|
||||||
*/
|
|
||||||
fun isUnavailableNetwork(context: Context, allowMetered: Boolean): Boolean {
|
|
||||||
return requiresNetwork && !hasUnmeteredInternet(context, allowMetered)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hasUnmeteredInternet(context: Context, allowMetered: Boolean): Boolean {
|
|
||||||
val cm = context.getSystemService(ConnectivityManager::class.java) ?: return false
|
|
||||||
val isMetered = cm.isActiveNetworkMetered
|
|
||||||
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
|
|
||||||
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
|
||||||
(allowMetered || !isMetered)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
package com.stevesoltys.seedvault.plugins.webdav
|
package com.stevesoltys.seedvault.plugins.webdav
|
||||||
|
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderLegacyPlugin
|
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
|
|
||||||
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 webDavModule = module {
|
val webDavModule = module {
|
||||||
// TODO PluginManager should create the plugin on demand
|
// TODO PluginManager should create the plugin on demand
|
||||||
single<StoragePlugin> { WebDavStoragePlugin(androidContext(), WebDavConfig("", "", "")) }
|
single<StoragePlugin<*>> { WebDavStoragePlugin(androidContext(), WebDavConfig("", "", "")) }
|
||||||
|
|
||||||
single { DocumentsStorage(androidContext(), get()) }
|
|
||||||
@Suppress("Deprecation")
|
|
||||||
single<LegacyStoragePlugin> { DocumentsProviderLegacyPlugin(androidContext(), get()) }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ import com.stevesoltys.seedvault.plugins.chunkFolderRegex
|
||||||
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
||||||
import com.stevesoltys.seedvault.plugins.saf.FILE_NO_MEDIA
|
import com.stevesoltys.seedvault.plugins.saf.FILE_NO_MEDIA
|
||||||
import com.stevesoltys.seedvault.plugins.tokenRegex
|
import com.stevesoltys.seedvault.plugins.tokenRegex
|
||||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
@ -25,7 +24,7 @@ internal class WebDavStoragePlugin(
|
||||||
context: Context,
|
context: Context,
|
||||||
webDavConfig: WebDavConfig,
|
webDavConfig: WebDavConfig,
|
||||||
root: String = DIRECTORY_ROOT,
|
root: String = DIRECTORY_ROOT,
|
||||||
) : WebDavStorage(webDavConfig, root), StoragePlugin {
|
) : WebDavStorage(webDavConfig, root), StoragePlugin<WebDavConfig> {
|
||||||
|
|
||||||
override suspend fun test(): Boolean {
|
override suspend fun test(): Boolean {
|
||||||
val location = baseUrl.toHttpUrl()
|
val location = baseUrl.toHttpUrl()
|
||||||
|
@ -134,12 +133,6 @@ internal class WebDavStoragePlugin(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override suspend fun hasBackup(safStorage: SafStorage): Boolean {
|
|
||||||
// TODO this requires refactoring
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getAvailableBackups(): Sequence<EncryptedMetadata>? {
|
override suspend fun getAvailableBackups(): Sequence<EncryptedMetadata>? {
|
||||||
return try {
|
return try {
|
||||||
doGetAvailableBackups()
|
doGetAvailableBackups()
|
||||||
|
|
|
@ -28,6 +28,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.metadata.PackageState.WAS_STOPPED
|
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
|
||||||
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
|
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
|
||||||
|
@ -81,8 +82,9 @@ internal class RestoreViewModel(
|
||||||
private val restoreCoordinator: RestoreCoordinator,
|
private val restoreCoordinator: RestoreCoordinator,
|
||||||
private val apkRestore: ApkRestore,
|
private val apkRestore: ApkRestore,
|
||||||
storageBackup: StorageBackup,
|
storageBackup: StorageBackup,
|
||||||
|
pluginManager: StoragePluginManager,
|
||||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager),
|
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager),
|
||||||
RestorableBackupClickListener, SnapshotViewModel {
|
RestorableBackupClickListener, SnapshotViewModel {
|
||||||
|
|
||||||
override val isRestoreOperation = true
|
override val isRestoreOperation = true
|
||||||
|
|
|
@ -10,6 +10,7 @@ import com.stevesoltys.seedvault.metadata.ApkSplit
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.restore.RestorableBackup
|
import com.stevesoltys.seedvault.restore.RestorableBackup
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
|
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
|
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
|
||||||
|
@ -28,7 +29,7 @@ private val TAG = ApkRestore::class.java.simpleName
|
||||||
|
|
||||||
internal class ApkRestore(
|
internal class ApkRestore(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val storagePlugin: StoragePlugin,
|
private val pluginManager: StoragePluginManager,
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
private val legacyStoragePlugin: LegacyStoragePlugin,
|
private val legacyStoragePlugin: LegacyStoragePlugin,
|
||||||
private val crypto: Crypto,
|
private val crypto: Crypto,
|
||||||
|
@ -37,6 +38,7 @@ internal class ApkRestore(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val pm = context.packageManager
|
private val pm = context.packageManager
|
||||||
|
private val storagePlugin get() = pluginManager.appPlugin
|
||||||
|
|
||||||
fun restore(backup: RestorableBackup) = flow {
|
fun restore(backup: RestorableBackup) = flow {
|
||||||
// we don't filter out apps without APK, so the user can manually install them
|
// we don't filter out apps without APK, so the user can manually install them
|
||||||
|
@ -87,7 +89,7 @@ internal class ApkRestore(
|
||||||
emit(installResult)
|
emit(installResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("ThrowsCount", "BlockingMethodInNonBlockingContext") // flows on Dispatcher.IO
|
@Suppress("ThrowsCount")
|
||||||
@Throws(IOException::class, SecurityException::class)
|
@Throws(IOException::class, SecurityException::class)
|
||||||
private suspend fun restore(
|
private suspend fun restore(
|
||||||
collector: FlowCollector<InstallResult>,
|
collector: FlowCollector<InstallResult>,
|
||||||
|
@ -212,7 +214,6 @@ internal class ApkRestore(
|
||||||
* @return a [Pair] of the cached [File] and SHA-256 hash.
|
* @return a [Pair] of the cached [File] and SHA-256 hash.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
@Suppress("BlockingMethodInNonBlockingContext") // flows on Dispatcher.IO
|
|
||||||
private suspend fun cacheApk(
|
private suspend fun cacheApk(
|
||||||
version: Byte,
|
version: Byte,
|
||||||
token: Long,
|
token: Long,
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
package com.stevesoltys.seedvault.settings
|
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.provider.Settings
|
|
||||||
|
|
||||||
private val SETTING = Settings.Secure.BACKUP_MANAGER_CONSTANTS
|
|
||||||
|
|
||||||
object BackupManagerSettings {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This clears the backup settings, so that default values will be used.
|
|
||||||
*
|
|
||||||
* Before end of 2020 (Android 11) we changed the settings in an attempt
|
|
||||||
* to prevent automatic backups when flash drives are not plugged in.
|
|
||||||
* This turned out to not work reliably, so reset to defaults again here.
|
|
||||||
*
|
|
||||||
* We can remove this code after the last users can be expected
|
|
||||||
* to have changed storage at least once with this code deployed.
|
|
||||||
*/
|
|
||||||
fun resetDefaults(resolver: ContentResolver) {
|
|
||||||
if (Settings.Secure.getString(resolver, SETTING) != null) {
|
|
||||||
// setting this to null will cause the BackupManagerConstants to use default values
|
|
||||||
Settings.Secure.putString(resolver, SETTING, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -10,6 +10,7 @@ import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
|
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.permitDiskReads
|
import com.stevesoltys.seedvault.permitDiskReads
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ class SchedulingFragment : PreferenceFragmentCompat(),
|
||||||
|
|
||||||
private val viewModel: SettingsViewModel by sharedViewModel()
|
private val viewModel: SettingsViewModel by sharedViewModel()
|
||||||
private val settingsManager: SettingsManager by inject()
|
private val settingsManager: SettingsManager by inject()
|
||||||
|
private val storagePluginManager: StoragePluginManager by inject()
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
permitDiskReads {
|
permitDiskReads {
|
||||||
|
@ -29,7 +31,7 @@ class SchedulingFragment : PreferenceFragmentCompat(),
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
val storage = settingsManager.getSafStorage()
|
val storage = storagePluginManager.storageProperties
|
||||||
if (storage?.isUsb == true) {
|
if (storage?.isUsb == true) {
|
||||||
findPreference<PreferenceCategory>("scheduling_category_conditions")?.isEnabled = false
|
findPreference<PreferenceCategory>("scheduling_category_conditions")?.isEnabled = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,8 @@ import androidx.preference.TwoStatePreference
|
||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.permitDiskReads
|
import com.stevesoltys.seedvault.permitDiskReads
|
||||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
|
import com.stevesoltys.seedvault.plugins.StorageProperties
|
||||||
import com.stevesoltys.seedvault.restore.RestoreActivity
|
import com.stevesoltys.seedvault.restore.RestoreActivity
|
||||||
import com.stevesoltys.seedvault.ui.toRelativeTime
|
import com.stevesoltys.seedvault.ui.toRelativeTime
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
@ -34,7 +35,7 @@ private val TAG = SettingsFragment::class.java.name
|
||||||
class SettingsFragment : PreferenceFragmentCompat() {
|
class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
private val viewModel: SettingsViewModel by sharedViewModel()
|
private val viewModel: SettingsViewModel by sharedViewModel()
|
||||||
private val settingsManager: SettingsManager by inject()
|
private val storagePluginManager: StoragePluginManager by inject()
|
||||||
private val backupManager: IBackupManager by inject()
|
private val backupManager: IBackupManager by inject()
|
||||||
|
|
||||||
private lateinit var backup: TwoStatePreference
|
private lateinit var backup: TwoStatePreference
|
||||||
|
@ -49,7 +50,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
private var menuBackupNow: MenuItem? = null
|
private var menuBackupNow: MenuItem? = null
|
||||||
private var menuRestore: MenuItem? = null
|
private var menuRestore: MenuItem? = null
|
||||||
|
|
||||||
private var safStorage: SafStorage? = null
|
private val storageProperties: StorageProperties<*>?
|
||||||
|
get() = storagePluginManager.storageProperties
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
permitDiskReads {
|
permitDiskReads {
|
||||||
|
@ -165,7 +167,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
// we need to re-set the title when returning to this fragment
|
// we need to re-set the title when returning to this fragment
|
||||||
activity?.setTitle(R.string.backup)
|
activity?.setTitle(R.string.backup)
|
||||||
|
|
||||||
safStorage = settingsManager.getSafStorage()
|
|
||||||
setBackupEnabledState()
|
setBackupEnabledState()
|
||||||
setBackupLocationSummary()
|
setBackupLocationSummary()
|
||||||
setAutoRestoreState()
|
setAutoRestoreState()
|
||||||
|
@ -242,7 +243,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
activity?.contentResolver?.let {
|
activity?.contentResolver?.let {
|
||||||
autoRestore.isChecked = Settings.Secure.getInt(it, BACKUP_AUTO_RESTORE, 1) == 1
|
autoRestore.isChecked = Settings.Secure.getInt(it, BACKUP_AUTO_RESTORE, 1) == 1
|
||||||
}
|
}
|
||||||
val storage = this.safStorage
|
val storage = this.storageProperties
|
||||||
if (storage?.isUsb == true) {
|
if (storage?.isUsb == true) {
|
||||||
autoRestore.summary = getString(R.string.settings_auto_restore_summary) + "\n\n" +
|
autoRestore.summary = getString(R.string.settings_auto_restore_summary) + "\n\n" +
|
||||||
getString(R.string.settings_auto_restore_summary_usb, storage.name)
|
getString(R.string.settings_auto_restore_summary_usb, storage.name)
|
||||||
|
@ -253,7 +254,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
private fun setBackupLocationSummary() {
|
private fun setBackupLocationSummary() {
|
||||||
// get name of storage location
|
// get name of storage location
|
||||||
backupLocation.summary = safStorage?.name ?: getString(R.string.settings_backup_location_none)
|
backupLocation.summary =
|
||||||
|
storageProperties?.name ?: getString(R.string.settings_backup_location_none)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setAppBackupStatusSummary(lastBackupInMillis: Long?) {
|
private fun setAppBackupStatusSummary(lastBackupInMillis: Long?) {
|
||||||
|
@ -272,7 +274,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
* says that nothing is scheduled which can happen when backup destination is on flash drive.
|
* says that nothing is scheduled which can happen when backup destination is on flash drive.
|
||||||
*/
|
*/
|
||||||
private fun setAppBackupSchedulingSummary(workInfo: WorkInfo?) {
|
private fun setAppBackupSchedulingSummary(workInfo: WorkInfo?) {
|
||||||
if (safStorage?.isUsb == true) {
|
if (storageProperties?.isUsb == true) {
|
||||||
backupScheduling.summary = getString(R.string.settings_backup_status_next_backup_usb)
|
backupScheduling.summary = getString(R.string.settings_backup_status_next_backup_usb)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,14 @@ package com.stevesoltys.seedvault.settings
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
import android.hardware.usb.UsbDevice
|
import android.hardware.usb.UsbDevice
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.annotation.UiThread
|
import androidx.annotation.UiThread
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.stevesoltys.seedvault.getStorageContext
|
import com.stevesoltys.seedvault.getStorageContext
|
||||||
import com.stevesoltys.seedvault.permitDiskReads
|
import com.stevesoltys.seedvault.permitDiskReads
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
||||||
import java.util.concurrent.ConcurrentSkipListSet
|
import java.util.concurrent.ConcurrentSkipListSet
|
||||||
|
@ -23,6 +22,12 @@ internal const val PREF_KEY_SCHED_FREQ = "scheduling_frequency"
|
||||||
internal const val PREF_KEY_SCHED_METERED = "scheduling_metered"
|
internal const val PREF_KEY_SCHED_METERED = "scheduling_metered"
|
||||||
internal const val PREF_KEY_SCHED_CHARGING = "scheduling_charging"
|
internal const val PREF_KEY_SCHED_CHARGING = "scheduling_charging"
|
||||||
|
|
||||||
|
private const val PREF_KEY_STORAGE_PLUGIN = "storagePlugin"
|
||||||
|
|
||||||
|
internal enum class StoragePluginEnum { // don't rename, will break existing installs
|
||||||
|
SAF,
|
||||||
|
}
|
||||||
|
|
||||||
private const val PREF_KEY_STORAGE_URI = "storageUri"
|
private const val PREF_KEY_STORAGE_URI = "storageUri"
|
||||||
private const val PREF_KEY_STORAGE_NAME = "storageName"
|
private const val PREF_KEY_STORAGE_NAME = "storageName"
|
||||||
private const val PREF_KEY_STORAGE_IS_USB = "storageIsUsb"
|
private const val PREF_KEY_STORAGE_IS_USB = "storageIsUsb"
|
||||||
|
@ -90,6 +95,25 @@ class SettingsManager(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME SafStorage is currently plugin specific and not generic
|
// FIXME SafStorage is currently plugin specific and not generic
|
||||||
|
internal val storagePluginType: StoragePluginEnum?
|
||||||
|
get() = prefs.getString(PREF_KEY_STORAGE_PLUGIN, StoragePluginEnum.SAF.name)?.let {
|
||||||
|
try {
|
||||||
|
StoragePluginEnum.valueOf(it)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStoragePlugin(plugin: StoragePlugin<*>) {
|
||||||
|
val value = when (plugin) {
|
||||||
|
is DocumentsProviderStoragePlugin -> StoragePluginEnum.SAF
|
||||||
|
else -> error("Unsupported plugin: ${plugin::class.java.simpleName}")
|
||||||
|
}.name
|
||||||
|
prefs.edit()
|
||||||
|
.putString(PREF_KEY_STORAGE_PLUGIN, value)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
fun setSafStorage(safStorage: SafStorage) {
|
fun setSafStorage(safStorage: SafStorage) {
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString(PREF_KEY_STORAGE_URI, safStorage.uri.toString())
|
.putString(PREF_KEY_STORAGE_URI, safStorage.uri.toString())
|
||||||
|
@ -196,42 +220,6 @@ class SettingsManager(private val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Storage(
|
|
||||||
val uri: Uri,
|
|
||||||
val name: String,
|
|
||||||
val isUsb: Boolean,
|
|
||||||
val requiresNetwork: Boolean,
|
|
||||||
) {
|
|
||||||
fun getDocumentFile(context: Context) = DocumentFile.fromTreeUri(context, uri)
|
|
||||||
?: throw AssertionError("Should only happen on API < 21.")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if this is USB storage that is not available, false otherwise.
|
|
||||||
*
|
|
||||||
* Must be run off UI thread (ideally I/O).
|
|
||||||
*/
|
|
||||||
@WorkerThread
|
|
||||||
fun isUnavailableUsb(context: Context): Boolean {
|
|
||||||
return isUsb && !getDocumentFile(context).isDirectory
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if this is storage that requires network access,
|
|
||||||
* but it isn't available right now.
|
|
||||||
*/
|
|
||||||
fun isUnavailableNetwork(context: Context, allowMetered: Boolean): Boolean {
|
|
||||||
return requiresNetwork && !hasUnmeteredInternet(context, allowMetered)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hasUnmeteredInternet(context: Context, allowMetered: Boolean): Boolean {
|
|
||||||
val cm = context.getSystemService(ConnectivityManager::class.java) ?: return false
|
|
||||||
val isMetered = cm.isActiveNetworkMetered
|
|
||||||
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
|
|
||||||
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
|
||||||
(allowMetered || !isMetered)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class FlashDrive(
|
data class FlashDrive(
|
||||||
val name: String,
|
val name: String,
|
||||||
val serialNumber: String?,
|
val serialNumber: String?,
|
||||||
|
|
|
@ -34,6 +34,8 @@ import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
import com.stevesoltys.seedvault.permitDiskReads
|
import com.stevesoltys.seedvault.permitDiskReads
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
|
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
||||||
import com.stevesoltys.seedvault.storage.StorageBackupJobService
|
import com.stevesoltys.seedvault.storage.StorageBackupJobService
|
||||||
import com.stevesoltys.seedvault.storage.StorageBackupService
|
import com.stevesoltys.seedvault.storage.StorageBackupService
|
||||||
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
|
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
|
||||||
|
@ -59,12 +61,13 @@ internal class SettingsViewModel(
|
||||||
app: Application,
|
app: Application,
|
||||||
settingsManager: SettingsManager,
|
settingsManager: SettingsManager,
|
||||||
keyManager: KeyManager,
|
keyManager: KeyManager,
|
||||||
|
private val pluginManager: StoragePluginManager,
|
||||||
private val metadataManager: MetadataManager,
|
private val metadataManager: MetadataManager,
|
||||||
private val appListRetriever: AppListRetriever,
|
private val appListRetriever: AppListRetriever,
|
||||||
private val storageBackup: StorageBackup,
|
private val storageBackup: StorageBackup,
|
||||||
private val backupManager: IBackupManager,
|
private val backupManager: IBackupManager,
|
||||||
private val backupInitializer: BackupInitializer,
|
private val backupInitializer: BackupInitializer,
|
||||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager) {
|
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager) {
|
||||||
|
|
||||||
private val contentResolver = app.contentResolver
|
private val contentResolver = app.contentResolver
|
||||||
private val connectivityManager: ConnectivityManager? =
|
private val connectivityManager: ConnectivityManager? =
|
||||||
|
@ -131,9 +134,9 @@ internal class SettingsViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStorageLocationChanged() {
|
override fun onStorageLocationChanged() {
|
||||||
val storage = settingsManager.getSafStorage() ?: return
|
val storage = pluginManager.storageProperties ?: return
|
||||||
|
|
||||||
Log.i(TAG, "onStorageLocationChanged (isUsb: ${storage.isUsb}")
|
Log.i(TAG, "onStorageLocationChanged (isUsb: ${storage.isUsb})")
|
||||||
if (storage.isUsb) {
|
if (storage.isUsb) {
|
||||||
// disable storage backup if new storage is on USB
|
// disable storage backup if new storage is on USB
|
||||||
cancelAppBackup()
|
cancelAppBackup()
|
||||||
|
@ -149,24 +152,27 @@ internal class SettingsViewModel(
|
||||||
|
|
||||||
fun onWorkerStateChanged() {
|
fun onWorkerStateChanged() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val canDo = settingsManager.canDoBackupNow() &&
|
val canDo = pluginManager.canDoBackupNow() &&
|
||||||
appBackupWorkInfo.value?.state != WorkInfo.State.RUNNING
|
appBackupWorkInfo.value?.state != WorkInfo.State.RUNNING
|
||||||
mBackupPossible.postValue(canDo)
|
mBackupPossible.postValue(canDo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onStoragePropertiesChanged() {
|
private fun onStoragePropertiesChanged() {
|
||||||
val storage = settingsManager.getSafStorage() ?: return
|
val storage = pluginManager.storageProperties ?: return
|
||||||
|
|
||||||
Log.d(TAG, "onStoragePropertiesChanged")
|
Log.d(TAG, "onStoragePropertiesChanged")
|
||||||
// register storage observer
|
if (storage is SafStorage) {
|
||||||
try {
|
// register storage observer
|
||||||
contentResolver.unregisterContentObserver(storageObserver)
|
try {
|
||||||
contentResolver.registerContentObserver(storage.uri, false, storageObserver)
|
contentResolver.unregisterContentObserver(storageObserver)
|
||||||
} catch (e: SecurityException) {
|
contentResolver.registerContentObserver(storage.uri, false, storageObserver)
|
||||||
// This can happen if the app providing the storage was uninstalled.
|
} catch (e: SecurityException) {
|
||||||
// validLocationIsSet() gets called elsewhere and prompts for a new storage location.
|
// This can happen if the app providing the storage was uninstalled.
|
||||||
Log.e(TAG, "Error registering content observer for ${storage.uri}", e)
|
// validLocationIsSet() gets called elsewhere
|
||||||
|
// and prompts for a new storage location.
|
||||||
|
Log.e(TAG, "Error registering content observer for ${storage.uri}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// register network observer if needed
|
// register network observer if needed
|
||||||
|
@ -301,7 +307,7 @@ internal class SettingsViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelAppBackup() {
|
private fun cancelAppBackup() {
|
||||||
AppBackupWorker.unschedule(app)
|
AppBackupWorker.unschedule(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,12 +16,8 @@ internal class SeedvaultSafStoragePlugin(
|
||||||
/**
|
/**
|
||||||
* Attention: This context might be from a different user. Use with care.
|
* Attention: This context might be from a different user. Use with care.
|
||||||
*/
|
*/
|
||||||
override val context: Context
|
override val context: Context get() = appContext.getStorageContext { storage.safStorage.isUsb }
|
||||||
get() = appContext.getStorageContext {
|
override val root: DocumentFile get() = storage.rootBackupDir ?: error("No storage set")
|
||||||
storage.safStorage?.isUsb == true
|
|
||||||
}
|
|
||||||
override val root: DocumentFile
|
|
||||||
get() = storage.rootBackupDir ?: error("No storage set")
|
|
||||||
|
|
||||||
override fun getMasterKey(): SecretKey = keyManager.getMainKey()
|
override fun getMasterKey(): SecretKey = keyManager.getMainKey()
|
||||||
override fun hasMasterKey(): Boolean = keyManager.hasMainKey()
|
override fun hasMasterKey(): Boolean = keyManager.hasMainKey()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package com.stevesoltys.seedvault.storage
|
package com.stevesoltys.seedvault.storage
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.worker.AppBackupWorker
|
import com.stevesoltys.seedvault.worker.AppBackupWorker
|
||||||
import org.calyxos.backup.storage.api.BackupObserver
|
import org.calyxos.backup.storage.api.BackupObserver
|
||||||
import org.calyxos.backup.storage.api.RestoreObserver
|
import org.calyxos.backup.storage.api.RestoreObserver
|
||||||
|
@ -34,7 +34,7 @@ internal class StorageBackupService : BackupService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override val storageBackup: StorageBackup by inject()
|
override val storageBackup: StorageBackup by inject()
|
||||||
private val settingsManager: SettingsManager by inject()
|
private val storagePluginManager: StoragePluginManager by inject()
|
||||||
|
|
||||||
// use lazy delegate because context isn't available during construction time
|
// use lazy delegate because context isn't available during construction time
|
||||||
override val backupObserver: BackupObserver by lazy {
|
override val backupObserver: BackupObserver by lazy {
|
||||||
|
@ -43,7 +43,7 @@ internal class StorageBackupService : BackupService() {
|
||||||
|
|
||||||
override fun onBackupFinished(intent: Intent, success: Boolean) {
|
override fun onBackupFinished(intent: Intent, success: Boolean) {
|
||||||
if (intent.getBooleanExtra(EXTRA_START_APP_BACKUP, false)) {
|
if (intent.getBooleanExtra(EXTRA_START_APP_BACKUP, false)) {
|
||||||
val isUsb = settingsManager.getSafStorage()?.isUsb ?: false
|
val isUsb = storagePluginManager.storageProperties?.isUsb ?: false
|
||||||
AppBackupWorker.scheduleNow(applicationContext, reschedule = !isUsb)
|
AppBackupWorker.scheduleNow(applicationContext, reschedule = !isUsb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
package com.stevesoltys.seedvault.storage
|
package com.stevesoltys.seedvault.storage
|
||||||
|
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import org.calyxos.backup.storage.api.StorageBackup
|
import org.calyxos.backup.storage.api.StorageBackup
|
||||||
import org.calyxos.backup.storage.api.StoragePlugin
|
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val storageModule = module {
|
val storageModule = module {
|
||||||
single<StoragePlugin> { SeedvaultSafStoragePlugin(get(), get(), get()) }
|
single { StorageBackup(get(), { get<StoragePluginManager>().filesPlugin }) }
|
||||||
single { StorageBackup(get(), get()) }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,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.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
|
@ -54,11 +55,10 @@ private class CoordinatorState(
|
||||||
* @author Steve Soltys
|
* @author Steve Soltys
|
||||||
* @author Torsten Grote
|
* @author Torsten Grote
|
||||||
*/
|
*/
|
||||||
@WorkerThread // entire class should always be accessed from a worker thread, so blocking is ok
|
@WorkerThread
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class BackupCoordinator(
|
internal class BackupCoordinator(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val plugin: StoragePlugin,
|
private val pluginManager: StoragePluginManager,
|
||||||
private val kv: KVBackup,
|
private val kv: KVBackup,
|
||||||
private val full: FullBackup,
|
private val full: FullBackup,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
|
@ -68,6 +68,7 @@ internal class BackupCoordinator(
|
||||||
private val nm: BackupNotificationManager,
|
private val nm: BackupNotificationManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val plugin get() = pluginManager.appPlugin
|
||||||
private val state = CoordinatorState(
|
private val state = CoordinatorState(
|
||||||
calledInitialize = false,
|
calledInitialize = false,
|
||||||
calledClearBackupData = false,
|
calledClearBackupData = false,
|
||||||
|
@ -126,7 +127,7 @@ internal class BackupCoordinator(
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error initializing device", e)
|
Log.e(TAG, "Error initializing device", e)
|
||||||
// Show error notification if we needed init or were ready for backups
|
// Show error notification if we needed init or were ready for backups
|
||||||
if (metadataManager.requiresInit || settingsManager.canDoBackupNow()) nm.onBackupError()
|
if (metadataManager.requiresInit || pluginManager.canDoBackupNow()) nm.onBackupError()
|
||||||
TRANSPORT_ERROR
|
TRANSPORT_ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,7 +355,7 @@ internal class BackupCoordinator(
|
||||||
if (result == TRANSPORT_OK) {
|
if (result == TRANSPORT_OK) {
|
||||||
val isNormalBackup = packageName != MAGIC_PACKAGE_MANAGER
|
val isNormalBackup = packageName != MAGIC_PACKAGE_MANAGER
|
||||||
// call onPackageBackedUp for @pm@ only if we can do backups right now
|
// call onPackageBackedUp for @pm@ only if we can do backups right now
|
||||||
if (isNormalBackup || settingsManager.canDoBackupNow()) {
|
if (isNormalBackup || pluginManager.canDoBackupNow()) {
|
||||||
try {
|
try {
|
||||||
onPackageBackedUp(packageInfo, BackupType.KV, size)
|
onPackageBackedUp(packageInfo, BackupType.KV, size)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -411,7 +412,7 @@ internal class BackupCoordinator(
|
||||||
val longBackoff = DAYS.toMillis(30)
|
val longBackoff = DAYS.toMillis(30)
|
||||||
|
|
||||||
// back off if there's no storage set
|
// back off if there's no storage set
|
||||||
val storage = settingsManager.getSafStorage() ?: return longBackoff
|
val storage = pluginManager.storageProperties ?: return longBackoff
|
||||||
return when {
|
return when {
|
||||||
// back off if storage is removable and not available right now
|
// back off if storage is removable and not available right now
|
||||||
storage.isUnavailableUsb(context) -> longBackoff
|
storage.isUnavailableUsb(context) -> longBackoff
|
||||||
|
@ -425,7 +426,9 @@ internal class BackupCoordinator(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun StoragePlugin.getMetadataOutputStream(token: Long? = null): OutputStream {
|
private suspend fun StoragePlugin<*>.getMetadataOutputStream(
|
||||||
|
token: Long? = null,
|
||||||
|
): OutputStream {
|
||||||
val t = token ?: settingsManager.getToken() ?: throw IOException("no current token")
|
val t = token ?: settingsManager.getToken() ?: throw IOException("no current token")
|
||||||
return getOutputStream(t, FILE_BACKUP_METADATA)
|
return getOutputStream(t, FILE_BACKUP_METADATA)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,38 +11,38 @@ val backupModule = module {
|
||||||
context = androidContext(),
|
context = androidContext(),
|
||||||
backupManager = get(),
|
backupManager = get(),
|
||||||
settingsManager = get(),
|
settingsManager = get(),
|
||||||
plugin = get()
|
pluginManager = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
single<KvDbManager> { KvDbManagerImpl(androidContext()) }
|
single<KvDbManager> { KvDbManagerImpl(androidContext()) }
|
||||||
single {
|
single {
|
||||||
KVBackup(
|
KVBackup(
|
||||||
plugin = get(),
|
pluginManager = get(),
|
||||||
settingsManager = get(),
|
settingsManager = get(),
|
||||||
inputFactory = get(),
|
inputFactory = get(),
|
||||||
crypto = get(),
|
crypto = get(),
|
||||||
dbManager = get()
|
dbManager = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
single {
|
single {
|
||||||
FullBackup(
|
FullBackup(
|
||||||
plugin = get(),
|
pluginManager = get(),
|
||||||
settingsManager = get(),
|
settingsManager = get(),
|
||||||
inputFactory = get(),
|
inputFactory = get(),
|
||||||
crypto = get()
|
crypto = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
single {
|
single {
|
||||||
BackupCoordinator(
|
BackupCoordinator(
|
||||||
context = androidContext(),
|
context = androidContext(),
|
||||||
plugin = get(),
|
pluginManager = get(),
|
||||||
kv = get(),
|
kv = get(),
|
||||||
full = get(),
|
full = get(),
|
||||||
clock = get(),
|
clock = get(),
|
||||||
packageService = get(),
|
packageService = get(),
|
||||||
metadataManager = get(),
|
metadataManager = get(),
|
||||||
settingsManager = get(),
|
settingsManager = get(),
|
||||||
nm = get()
|
nm = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import android.util.Log
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.crypto.Crypto
|
||||||
import com.stevesoltys.seedvault.header.VERSION
|
import com.stevesoltys.seedvault.header.VERSION
|
||||||
import com.stevesoltys.seedvault.header.getADForFull
|
import com.stevesoltys.seedvault.header.getADForFull
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import libcore.io.IoUtils.closeQuietly
|
import libcore.io.IoUtils.closeQuietly
|
||||||
import java.io.EOFException
|
import java.io.EOFException
|
||||||
|
@ -39,12 +39,13 @@ private val TAG = FullBackup::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class FullBackup(
|
internal class FullBackup(
|
||||||
private val plugin: StoragePlugin,
|
private val pluginManager: StoragePluginManager,
|
||||||
private val settingsManager: SettingsManager,
|
private val settingsManager: SettingsManager,
|
||||||
private val inputFactory: InputFactory,
|
private val inputFactory: InputFactory,
|
||||||
private val crypto: Crypto,
|
private val crypto: Crypto,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val plugin get() = pluginManager.appPlugin
|
||||||
private var state: FullBackupState? = null
|
private var state: FullBackupState? = null
|
||||||
|
|
||||||
fun hasState() = state != null
|
fun hasState() = state != null
|
||||||
|
|
|
@ -13,7 +13,7 @@ import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.crypto.Crypto
|
import com.stevesoltys.seedvault.crypto.Crypto
|
||||||
import com.stevesoltys.seedvault.header.VERSION
|
import com.stevesoltys.seedvault.header.VERSION
|
||||||
import com.stevesoltys.seedvault.header.getADForKV
|
import com.stevesoltys.seedvault.header.getADForKV
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
|
@ -31,15 +31,15 @@ const val DEFAULT_QUOTA_KEY_VALUE_BACKUP = (2 * (5 * 1024 * 1024)).toLong()
|
||||||
|
|
||||||
private val TAG = KVBackup::class.java.simpleName
|
private val TAG = KVBackup::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class KVBackup(
|
internal class KVBackup(
|
||||||
private val plugin: StoragePlugin,
|
private val pluginManager: StoragePluginManager,
|
||||||
private val settingsManager: SettingsManager,
|
private val settingsManager: SettingsManager,
|
||||||
private val inputFactory: InputFactory,
|
private val inputFactory: InputFactory,
|
||||||
private val crypto: Crypto,
|
private val crypto: Crypto,
|
||||||
private val dbManager: KvDbManager,
|
private val dbManager: KvDbManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val plugin get() = pluginManager.appPlugin
|
||||||
private var state: KVBackupState? = null
|
private var state: KVBackupState? = null
|
||||||
|
|
||||||
fun hasState() = state != null
|
fun hasState() = state != null
|
||||||
|
@ -138,7 +138,7 @@ internal class KVBackup(
|
||||||
// K/V backups (typically starting with package manager metadata - @pm@)
|
// K/V backups (typically starting with package manager metadata - @pm@)
|
||||||
// are scheduled with JobInfo.Builder#setOverrideDeadline()
|
// are scheduled with JobInfo.Builder#setOverrideDeadline()
|
||||||
// and thus do not respect backoff.
|
// and thus do not respect backoff.
|
||||||
settingsManager.canDoBackupNow()
|
pluginManager.canDoBackupNow()
|
||||||
} else {
|
} else {
|
||||||
// all other packages always need upload
|
// all other packages always need upload
|
||||||
true
|
true
|
||||||
|
|
|
@ -18,6 +18,7 @@ import android.util.Log.INFO
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
|
|
||||||
private val TAG = PackageService::class.java.simpleName
|
private val TAG = PackageService::class.java.simpleName
|
||||||
|
@ -32,11 +33,12 @@ internal class PackageService(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val backupManager: IBackupManager,
|
private val backupManager: IBackupManager,
|
||||||
private val settingsManager: SettingsManager,
|
private val settingsManager: SettingsManager,
|
||||||
private val plugin: StoragePlugin,
|
private val pluginManager: StoragePluginManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val packageManager: PackageManager = context.packageManager
|
private val packageManager: PackageManager = context.packageManager
|
||||||
private val myUserId = UserHandle.myUserId()
|
private val myUserId = UserHandle.myUserId()
|
||||||
|
private val plugin: StoragePlugin<*> get() = pluginManager.appPlugin
|
||||||
|
|
||||||
val eligiblePackages: List<String>
|
val eligiblePackages: List<String>
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
|
|
|
@ -13,7 +13,7 @@ import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH
|
||||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
||||||
import com.stevesoltys.seedvault.header.getADForFull
|
import com.stevesoltys.seedvault.header.getADForFull
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import libcore.io.IoUtils.closeQuietly
|
import libcore.io.IoUtils.closeQuietly
|
||||||
import java.io.EOFException
|
import java.io.EOFException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -32,9 +32,8 @@ private class FullRestoreState(
|
||||||
|
|
||||||
private val TAG = FullRestore::class.java.simpleName
|
private val TAG = FullRestore::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class FullRestore(
|
internal class FullRestore(
|
||||||
private val plugin: StoragePlugin,
|
private val pluginManager: StoragePluginManager,
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
private val legacyPlugin: LegacyStoragePlugin,
|
private val legacyPlugin: LegacyStoragePlugin,
|
||||||
private val outputFactory: OutputFactory,
|
private val outputFactory: OutputFactory,
|
||||||
|
@ -42,6 +41,7 @@ internal class FullRestore(
|
||||||
private val crypto: Crypto,
|
private val crypto: Crypto,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val plugin get() = pluginManager.appPlugin
|
||||||
private var state: FullRestoreState? = null
|
private var state: FullRestoreState? = null
|
||||||
|
|
||||||
fun hasState() = state != null
|
fun hasState() = state != null
|
||||||
|
|
|
@ -16,13 +16,12 @@ import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
||||||
import com.stevesoltys.seedvault.header.VERSION
|
import com.stevesoltys.seedvault.header.VERSION
|
||||||
import com.stevesoltys.seedvault.header.getADForKV
|
import com.stevesoltys.seedvault.header.getADForKV
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.transport.backup.KVDb
|
import com.stevesoltys.seedvault.transport.backup.KVDb
|
||||||
import com.stevesoltys.seedvault.transport.backup.KvDbManager
|
import com.stevesoltys.seedvault.transport.backup.KvDbManager
|
||||||
import libcore.io.IoUtils.closeQuietly
|
import libcore.io.IoUtils.closeQuietly
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.GeneralSecurityException
|
import java.security.GeneralSecurityException
|
||||||
import java.util.ArrayList
|
|
||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
import javax.crypto.AEADBadTagException
|
import javax.crypto.AEADBadTagException
|
||||||
|
|
||||||
|
@ -39,9 +38,8 @@ private class KVRestoreState(
|
||||||
|
|
||||||
private val TAG = KVRestore::class.java.simpleName
|
private val TAG = KVRestore::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class KVRestore(
|
internal class KVRestore(
|
||||||
private val plugin: StoragePlugin,
|
private val pluginManager: StoragePluginManager,
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
private val legacyPlugin: LegacyStoragePlugin,
|
private val legacyPlugin: LegacyStoragePlugin,
|
||||||
private val outputFactory: OutputFactory,
|
private val outputFactory: OutputFactory,
|
||||||
|
@ -50,6 +48,7 @@ internal class KVRestore(
|
||||||
private val dbManager: KvDbManager,
|
private val dbManager: KvDbManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val plugin get() = pluginManager.appPlugin
|
||||||
private var state: KVRestoreState? = null
|
private var state: KVRestoreState? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,6 +21,7 @@ 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.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS
|
import com.stevesoltys.seedvault.transport.D2D_TRANSPORT_FLAGS
|
||||||
import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS
|
import com.stevesoltys.seedvault.transport.DEFAULT_TRANSPORT_FLAGS
|
||||||
|
@ -49,19 +50,19 @@ private data class RestoreCoordinatorState(
|
||||||
|
|
||||||
private val TAG = RestoreCoordinator::class.java.simpleName
|
private val TAG = RestoreCoordinator::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class RestoreCoordinator(
|
internal class RestoreCoordinator(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val crypto: Crypto,
|
private val crypto: Crypto,
|
||||||
private val settingsManager: SettingsManager,
|
private val settingsManager: SettingsManager,
|
||||||
private val metadataManager: MetadataManager,
|
private val metadataManager: MetadataManager,
|
||||||
private val notificationManager: BackupNotificationManager,
|
private val notificationManager: BackupNotificationManager,
|
||||||
private val plugin: StoragePlugin,
|
private val pluginManager: StoragePluginManager,
|
||||||
private val kv: KVRestore,
|
private val kv: KVRestore,
|
||||||
private val full: FullRestore,
|
private val full: FullRestore,
|
||||||
private val metadataReader: MetadataReader,
|
private val metadataReader: MetadataReader,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val plugin: StoragePlugin<*> get() = pluginManager.appPlugin
|
||||||
private var state: RestoreCoordinatorState? = null
|
private var state: RestoreCoordinatorState? = null
|
||||||
private var backupMetadata: BackupMetadata? = null
|
private var backupMetadata: BackupMetadata? = null
|
||||||
private val failedPackages = ArrayList<String>()
|
private val failedPackages = ArrayList<String>()
|
||||||
|
@ -169,7 +170,7 @@ internal class RestoreCoordinator(
|
||||||
// check if we even have a backup of that app
|
// check if we even have a backup of that app
|
||||||
if (metadataManager.getPackageMetadata(pmPackageName) != null) {
|
if (metadataManager.getPackageMetadata(pmPackageName) != null) {
|
||||||
// remind user to plug in storage device
|
// remind user to plug in storage device
|
||||||
val storageName = settingsManager.getSafStorage()?.name
|
val storageName = pluginManager.storageProperties?.name
|
||||||
?: context.getString(R.string.settings_backup_location_none)
|
?: context.getString(R.string.settings_backup_location_none)
|
||||||
notificationManager.onRemovableStorageNotAvailableForRestore(
|
notificationManager.onRemovableStorageNotAvailableForRestore(
|
||||||
pmPackageName,
|
pmPackageName,
|
||||||
|
@ -363,9 +364,8 @@ internal class RestoreCoordinator(
|
||||||
|
|
||||||
fun isFailedPackage(packageName: String) = packageName in failedPackages
|
fun isFailedPackage(packageName: String) = packageName in failedPackages
|
||||||
|
|
||||||
// TODO this is plugin specific, needs to be factored out when supporting different plugins
|
|
||||||
private fun isStorageRemovableAndNotAvailable(): Boolean {
|
private fun isStorageRemovableAndNotAvailable(): Boolean {
|
||||||
val storage = settingsManager.getSafStorage() ?: return false
|
val storage = pluginManager.storageProperties ?: return false
|
||||||
return storage.isUnavailableUsb(context)
|
return storage.isUnavailableUsb(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,14 @@ package com.stevesoltys.seedvault.ui
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.ui.storage.StorageViewModel
|
|
||||||
|
|
||||||
abstract class RequireProvisioningViewModel(
|
abstract class RequireProvisioningViewModel(
|
||||||
protected val app: Application,
|
protected val app: Application,
|
||||||
protected val settingsManager: SettingsManager,
|
protected val settingsManager: SettingsManager,
|
||||||
protected val keyManager: KeyManager,
|
protected val keyManager: KeyManager,
|
||||||
|
private val pluginManager: StoragePluginManager,
|
||||||
) : AndroidViewModel(app) {
|
) : AndroidViewModel(app) {
|
||||||
|
|
||||||
abstract val isRestoreOperation: Boolean
|
abstract val isRestoreOperation: Boolean
|
||||||
|
@ -18,7 +19,7 @@ abstract class RequireProvisioningViewModel(
|
||||||
internal val chooseBackupLocation: LiveEvent<Boolean> get() = mChooseBackupLocation
|
internal val chooseBackupLocation: LiveEvent<Boolean> get() = mChooseBackupLocation
|
||||||
internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true)
|
internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true)
|
||||||
|
|
||||||
internal fun validLocationIsSet() = StorageViewModel.validLocationIsSet(app, settingsManager)
|
internal fun validLocationIsSet() = pluginManager.isValidAppPluginSet()
|
||||||
|
|
||||||
internal fun recoveryCodeIsSet() = keyManager.hasBackupKey()
|
internal fun recoveryCodeIsSet() = keyManager.hasBackupKey()
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,13 @@ package com.stevesoltys.seedvault.ui.storage
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.backup.IBackupManager
|
import android.app.backup.IBackupManager
|
||||||
import android.app.job.JobInfo
|
import android.app.job.JobInfo
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
|
import com.stevesoltys.seedvault.plugins.saf.SafHandler
|
||||||
|
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.storage.StorageBackupJobService
|
import com.stevesoltys.seedvault.storage.StorageBackupJobService
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupInitializer
|
import com.stevesoltys.seedvault.transport.backup.BackupInitializer
|
||||||
|
@ -26,14 +28,17 @@ internal class BackupStorageViewModel(
|
||||||
private val backupManager: IBackupManager,
|
private val backupManager: IBackupManager,
|
||||||
private val backupInitializer: BackupInitializer,
|
private val backupInitializer: BackupInitializer,
|
||||||
private val storageBackup: StorageBackup,
|
private val storageBackup: StorageBackup,
|
||||||
|
safHandler: SafHandler,
|
||||||
settingsManager: SettingsManager,
|
settingsManager: SettingsManager,
|
||||||
) : StorageViewModel(app, settingsManager) {
|
storagePluginManager: StoragePluginManager,
|
||||||
|
) : StorageViewModel(app, safHandler, settingsManager, storagePluginManager) {
|
||||||
|
|
||||||
override val isRestoreOperation = false
|
override val isRestoreOperation = false
|
||||||
|
|
||||||
override fun onSafUriSet(uri: Uri) {
|
override fun onSafUriSet(safStorage: SafStorage) {
|
||||||
val isUsb = saveStorage(uri)
|
safHandler.save(safStorage)
|
||||||
if (isUsb) {
|
safHandler.setPlugin(safStorage)
|
||||||
|
if (safStorage.isUsb) {
|
||||||
// disable storage backup if new storage is on USB
|
// disable storage backup if new storage is on USB
|
||||||
cancelBackupWorkers()
|
cancelBackupWorkers()
|
||||||
} else {
|
} else {
|
||||||
|
@ -41,12 +46,16 @@ internal class BackupStorageViewModel(
|
||||||
// also to update the network requirement of the new storage
|
// also to update the network requirement of the new storage
|
||||||
scheduleBackupWorkers()
|
scheduleBackupWorkers()
|
||||||
}
|
}
|
||||||
|
onStorageLocationSet(safStorage.isUsb)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onStorageLocationSet(isUsb: Boolean) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
// remove old storage snapshots and clear cache
|
|
||||||
// TODO is this needed? It also does create all 255 chunk folders which takes time
|
|
||||||
// pass a flag to getCurrentBackupSnapshots() to not create missing folders?
|
|
||||||
storageBackup.init()
|
|
||||||
try {
|
try {
|
||||||
|
// remove old storage snapshots and clear cache
|
||||||
|
// TODO For SAF, this also does create all 255 chunk folders which takes time
|
||||||
|
// pass a flag to getCurrentBackupSnapshots() to not create missing folders?
|
||||||
|
storageBackup.init()
|
||||||
// initialize the new location (if backups are enabled)
|
// initialize the new location (if backups are enabled)
|
||||||
if (backupManager.isBackupEnabled) {
|
if (backupManager.isBackupEnabled) {
|
||||||
val onError = {
|
val onError = {
|
||||||
|
@ -74,7 +83,7 @@ internal class BackupStorageViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scheduleBackupWorkers() {
|
private fun scheduleBackupWorkers() {
|
||||||
val storage = settingsManager.getSafStorage() ?: error("no storage available")
|
val storage = storagePluginManager.storageProperties ?: error("no storage available")
|
||||||
if (!storage.isUsb) {
|
if (!storage.isUsb) {
|
||||||
if (backupManager.isBackupEnabled) {
|
if (backupManager.isBackupEnabled) {
|
||||||
AppBackupWorker.schedule(app, settingsManager, CANCEL_AND_REENQUEUE)
|
AppBackupWorker.schedule(app, settingsManager, CANCEL_AND_REENQUEUE)
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
package com.stevesoltys.seedvault.ui.storage
|
package com.stevesoltys.seedvault.ui.storage
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DIRECTORY_ROOT
|
import com.stevesoltys.seedvault.plugins.saf.DIRECTORY_ROOT
|
||||||
|
import com.stevesoltys.seedvault.plugins.saf.SafHandler
|
||||||
|
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -16,26 +17,27 @@ private val TAG = RestoreStorageViewModel::class.java.simpleName
|
||||||
|
|
||||||
internal class RestoreStorageViewModel(
|
internal class RestoreStorageViewModel(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val storagePlugin: StoragePlugin,
|
safHandler: SafHandler,
|
||||||
settingsManager: SettingsManager,
|
settingsManager: SettingsManager,
|
||||||
) : StorageViewModel(app, settingsManager) {
|
storagePluginManager: StoragePluginManager,
|
||||||
|
) : StorageViewModel(app, safHandler, settingsManager, storagePluginManager) {
|
||||||
|
|
||||||
override val isRestoreOperation = true
|
override val isRestoreOperation = true
|
||||||
|
|
||||||
override fun onSafUriSet(uri: Uri) {
|
override fun onSafUriSet(safStorage: SafStorage) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val storage = createStorage(uri)
|
|
||||||
val hasBackup = try {
|
val hasBackup = try {
|
||||||
storagePlugin.hasBackup(storage)
|
safHandler.hasAppBackup(safStorage)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error reading URI: $uri", e)
|
Log.e(TAG, "Error reading URI: ${safStorage.uri}", e)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
if (hasBackup) {
|
if (hasBackup) {
|
||||||
saveStorage(storage)
|
safHandler.save(safStorage)
|
||||||
|
safHandler.setPlugin(safStorage)
|
||||||
mLocationChecked.postEvent(LocationResult())
|
mLocationChecked.postEvent(LocationResult())
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Location was rejected: $uri")
|
Log.w(TAG, "Location was rejected: ${safStorage.uri}")
|
||||||
|
|
||||||
// notify the UI that the location was invalid
|
// notify the UI that the location was invalid
|
||||||
val errorMsg =
|
val errorMsg =
|
||||||
|
@ -44,5 +46,4 @@ internal class RestoreStorageViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,9 +62,7 @@ internal class StorageOptionFetcher(private val context: Context, private val is
|
||||||
internal fun getRemovableStorageListener() = listener
|
internal fun getRemovableStorageListener() = listener
|
||||||
|
|
||||||
internal fun getStorageOptions(): List<StorageOption> {
|
internal fun getStorageOptions(): List<StorageOption> {
|
||||||
val roots = ArrayList<StorageOption>().apply {
|
val roots = ArrayList<StorageOption>()
|
||||||
add(WebDavOption(context))
|
|
||||||
}
|
|
||||||
val intent = Intent(PROVIDER_INTERFACE)
|
val intent = Intent(PROVIDER_INTERFACE)
|
||||||
val providers = packageManager.queryIntentContentProviders(intent, 0)
|
val providers = packageManager.queryIntentContentProviders(intent, 0)
|
||||||
for (info in providers) {
|
for (info in providers) {
|
||||||
|
|
|
@ -2,23 +2,15 @@ package com.stevesoltys.seedvault.ui.storage
|
||||||
|
|
||||||
import android.annotation.UiThread
|
import android.annotation.UiThread
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
|
||||||
import android.content.Context.USB_SERVICE
|
|
||||||
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
||||||
import android.hardware.usb.UsbManager
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.isMassStorage
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.permitDiskReads
|
import com.stevesoltys.seedvault.plugins.saf.SafHandler
|
||||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
||||||
import com.stevesoltys.seedvault.settings.BackupManagerSettings
|
|
||||||
import com.stevesoltys.seedvault.settings.FlashDrive
|
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.ui.LiveEvent
|
import com.stevesoltys.seedvault.ui.LiveEvent
|
||||||
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
||||||
|
@ -26,11 +18,11 @@ import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
private val TAG = StorageViewModel::class.java.simpleName
|
|
||||||
|
|
||||||
internal abstract class StorageViewModel(
|
internal abstract class StorageViewModel(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
|
protected val safHandler: SafHandler,
|
||||||
protected val settingsManager: SettingsManager,
|
protected val settingsManager: SettingsManager,
|
||||||
|
protected val storagePluginManager: StoragePluginManager,
|
||||||
) : AndroidViewModel(app), RemovableStorageListener {
|
) : AndroidViewModel(app), RemovableStorageListener {
|
||||||
|
|
||||||
private val mStorageOptions = MutableLiveData<List<StorageOption>>()
|
private val mStorageOptions = MutableLiveData<List<StorageOption>>()
|
||||||
|
@ -47,22 +39,9 @@ internal abstract class StorageViewModel(
|
||||||
|
|
||||||
internal var isSetupWizard: Boolean = false
|
internal var isSetupWizard: Boolean = false
|
||||||
internal val hasStorageSet: Boolean
|
internal val hasStorageSet: Boolean
|
||||||
get() = settingsManager.getSafStorage() != null
|
get() = storagePluginManager.storageProperties != null
|
||||||
abstract val isRestoreOperation: Boolean
|
abstract val isRestoreOperation: Boolean
|
||||||
|
|
||||||
companion object {
|
|
||||||
internal fun validLocationIsSet(
|
|
||||||
context: Context,
|
|
||||||
settingsManager: SettingsManager,
|
|
||||||
): Boolean {
|
|
||||||
val storage = settingsManager.getSafStorage() ?: return false
|
|
||||||
if (storage.isUsb) return true
|
|
||||||
return permitDiskReads {
|
|
||||||
storage.getDocumentFile(context).isDirectory
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun loadStorageRoots() {
|
internal fun loadStorageRoots() {
|
||||||
if (storageOptionFetcher.getRemovableStorageListener() == null) {
|
if (storageOptionFetcher.getRemovableStorageListener() == null) {
|
||||||
storageOptionFetcher.setRemovableStorageListener(this)
|
storageOptionFetcher.setRemovableStorageListener(this)
|
||||||
|
@ -74,6 +53,10 @@ internal abstract class StorageViewModel(
|
||||||
|
|
||||||
override fun onStorageChanged() = loadStorageRoots()
|
override fun onStorageChanged() = loadStorageRoots()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remembers that the user chose SAF.
|
||||||
|
* Usually followed by a call of [onUriPermissionResultReceived].
|
||||||
|
*/
|
||||||
fun onSafOptionChosen(option: SafOption) {
|
fun onSafOptionChosen(option: SafOption) {
|
||||||
safOption = option
|
safOption = option
|
||||||
}
|
}
|
||||||
|
@ -84,71 +67,18 @@ internal abstract class StorageViewModel(
|
||||||
mLocationChecked.setEvent(LocationResult(msg))
|
mLocationChecked.setEvent(LocationResult(msg))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
require(safOption?.uri == uri) { "URIs differ: ${safOption?.uri} != $uri" }
|
||||||
|
|
||||||
|
val root = safOption ?: throw IllegalStateException("no storage root")
|
||||||
|
val safStorage = safHandler.onConfigReceived(uri, root)
|
||||||
|
|
||||||
// inform UI that a location has been successfully selected
|
// inform UI that a location has been successfully selected
|
||||||
mLocationSet.setEvent(true)
|
mLocationSet.setEvent(true)
|
||||||
|
|
||||||
// persist permission to access backup folder across reboots
|
onSafUriSet(safStorage)
|
||||||
val takeFlags = FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
|
|
||||||
app.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
|
||||||
|
|
||||||
onSafUriSet(uri)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
abstract fun onSafUriSet(safStorage: SafStorage)
|
||||||
* Saves the storage behind the given [Uri] (and saved [safOption]).
|
|
||||||
*
|
|
||||||
* @return true if the storage is a USB flash drive, false otherwise.
|
|
||||||
*/
|
|
||||||
protected fun saveStorage(uri: Uri): Boolean {
|
|
||||||
// store backup storage location in settings
|
|
||||||
val storage = createStorage(uri)
|
|
||||||
return saveStorage(storage)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun createStorage(uri: Uri): SafStorage {
|
|
||||||
val root = safOption ?: throw IllegalStateException("no storage root")
|
|
||||||
val name = if (root.isInternal()) {
|
|
||||||
"${root.title} (${app.getString(R.string.settings_backup_location_internal)})"
|
|
||||||
} else {
|
|
||||||
root.title
|
|
||||||
}
|
|
||||||
return SafStorage(uri, name, root.isUsb, root.requiresNetwork)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun saveStorage(safStorage: SafStorage): Boolean {
|
|
||||||
settingsManager.setSafStorage(safStorage)
|
|
||||||
|
|
||||||
if (safStorage.isUsb) {
|
|
||||||
Log.d(TAG, "Selected storage is a removable USB device.")
|
|
||||||
val wasSaved = saveUsbDevice()
|
|
||||||
// reset stored flash drive, if we did not update it
|
|
||||||
if (!wasSaved) settingsManager.setFlashDrive(null)
|
|
||||||
} else {
|
|
||||||
settingsManager.setFlashDrive(null)
|
|
||||||
}
|
|
||||||
BackupManagerSettings.resetDefaults(app.contentResolver)
|
|
||||||
|
|
||||||
Log.d(TAG, "New storage location saved: ${safStorage.uri}")
|
|
||||||
|
|
||||||
return safStorage.isUsb
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveUsbDevice(): Boolean {
|
|
||||||
val manager = app.getSystemService(USB_SERVICE) as UsbManager
|
|
||||||
manager.deviceList.values.forEach { device ->
|
|
||||||
if (device.isMassStorage()) {
|
|
||||||
val flashDrive = FlashDrive.from(device)
|
|
||||||
settingsManager.setFlashDrive(flashDrive)
|
|
||||||
Log.d(TAG, "Saved flash drive: $flashDrive")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.e(TAG, "No USB device found even though we were expecting one.")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun onSafUriSet(uri: Uri)
|
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
storageOptionFetcher.setRemovableStorageListener(null)
|
storageOptionFetcher.setRemovableStorageListener(null)
|
||||||
|
|
|
@ -30,7 +30,6 @@ import java.security.MessageDigest
|
||||||
|
|
||||||
private val TAG = ApkBackup::class.java.simpleName
|
private val TAG = ApkBackup::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class ApkBackup(
|
internal class ApkBackup(
|
||||||
private val pm: PackageManager,
|
private val pm: PackageManager,
|
||||||
private val crypto: Crypto,
|
private val crypto: Crypto,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||||
|
@ -28,7 +29,7 @@ internal class ApkBackupManager(
|
||||||
private val metadataManager: MetadataManager,
|
private val metadataManager: MetadataManager,
|
||||||
private val packageService: PackageService,
|
private val packageService: PackageService,
|
||||||
private val apkBackup: ApkBackup,
|
private val apkBackup: ApkBackup,
|
||||||
private val plugin: StoragePlugin,
|
private val pluginManager: StoragePluginManager,
|
||||||
private val nm: BackupNotificationManager,
|
private val nm: BackupNotificationManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -50,7 +51,7 @@ internal class ApkBackupManager(
|
||||||
keepTrying {
|
keepTrying {
|
||||||
// upload all local changes only at the end,
|
// upload all local changes only at the end,
|
||||||
// so we don't have to re-upload the metadata
|
// so we don't have to re-upload the metadata
|
||||||
plugin.getMetadataOutputStream().use { outputStream ->
|
pluginManager.appPlugin.getMetadataOutputStream().use { outputStream ->
|
||||||
metadataManager.uploadMetadata(outputStream)
|
metadataManager.uploadMetadata(outputStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,7 +103,7 @@ internal class ApkBackupManager(
|
||||||
return try {
|
return try {
|
||||||
apkBackup.backupApkIfNecessary(packageInfo) { name ->
|
apkBackup.backupApkIfNecessary(packageInfo) { name ->
|
||||||
val token = settingsManager.getToken() ?: throw IOException("no current token")
|
val token = settingsManager.getToken() ?: throw IOException("no current token")
|
||||||
plugin.getOutputStream(token, name)
|
pluginManager.appPlugin.getOutputStream(token, name)
|
||||||
}?.let { packageMetadata ->
|
}?.let { packageMetadata ->
|
||||||
metadataManager.onApkBackedUp(packageInfo, packageMetadata)
|
metadataManager.onApkBackedUp(packageInfo, packageMetadata)
|
||||||
true
|
true
|
||||||
|
@ -125,7 +126,9 @@ internal class ApkBackupManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun StoragePlugin.getMetadataOutputStream(token: Long? = null): OutputStream {
|
private suspend fun StoragePlugin<*>.getMetadataOutputStream(
|
||||||
|
token: Long? = null,
|
||||||
|
): OutputStream {
|
||||||
val t = token ?: settingsManager.getToken() ?: throw IOException("no current token")
|
val t = token ?: settingsManager.getToken() ?: throw IOException("no current token")
|
||||||
return getOutputStream(t, FILE_BACKUP_METADATA)
|
return getOutputStream(t, FILE_BACKUP_METADATA)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ val workerModule = module {
|
||||||
metadataManager = get(),
|
metadataManager = get(),
|
||||||
packageService = get(),
|
packageService = get(),
|
||||||
apkBackup = get(),
|
apkBackup = get(),
|
||||||
plugin = get(),
|
pluginManager = get(),
|
||||||
nm = get()
|
nm = get()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
|
import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
|
||||||
import com.stevesoltys.seedvault.header.headerModule
|
import com.stevesoltys.seedvault.header.headerModule
|
||||||
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.storagePluginModuleSaf
|
||||||
import com.stevesoltys.seedvault.restore.install.installModule
|
import com.stevesoltys.seedvault.restore.install.installModule
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.transport.backup.backupModule
|
import com.stevesoltys.seedvault.transport.backup.backupModule
|
||||||
|
@ -42,7 +42,7 @@ class TestApp : App() {
|
||||||
testCryptoModule,
|
testCryptoModule,
|
||||||
headerModule,
|
headerModule,
|
||||||
metadataModule,
|
metadataModule,
|
||||||
documentsProviderModule, // storage plugin
|
storagePluginModuleSaf, // storage plugin
|
||||||
backupModule,
|
backupModule,
|
||||||
restoreModule,
|
restoreModule,
|
||||||
installModule,
|
installModule,
|
||||||
|
|
|
@ -11,7 +11,6 @@ import io.mockk.mockkStatic
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class StoragePluginTest : BackupTest() {
|
internal class StoragePluginTest : BackupTest() {
|
||||||
|
|
||||||
private val storage = mockk<DocumentsStorage>()
|
private val storage = mockk<DocumentsStorage>()
|
||||||
|
@ -39,7 +38,7 @@ internal class StoragePluginTest : BackupTest() {
|
||||||
// get current set dir and for that the current token
|
// get current set dir and for that the current token
|
||||||
every { storage getProperty "currentToken" } returns token
|
every { storage getProperty "currentToken" } returns token
|
||||||
every { settingsManager.getToken() } returns token
|
every { settingsManager.getToken() } returns token
|
||||||
every { storage getProperty "storage" } returns null // just to check if isUsb
|
every { storage getProperty "safStorage" } returns null // just to check if isUsb
|
||||||
coEvery { storage.getSetDir(token) } returns setDir
|
coEvery { storage.getSetDir(token) } returns setDir
|
||||||
// delete contents of current set dir
|
// delete contents of current set dir
|
||||||
coEvery { setDir.listFilesBlocking(context) } returns listOf(backupFile)
|
coEvery { setDir.listFilesBlocking(context) } returns listOf(backupFile)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.restore.RestorableBackup
|
import com.stevesoltys.seedvault.restore.RestorableBackup
|
||||||
import com.stevesoltys.seedvault.transport.TransportTest
|
import com.stevesoltys.seedvault.transport.TransportTest
|
||||||
import com.stevesoltys.seedvault.worker.ApkBackup
|
import com.stevesoltys.seedvault.worker.ApkBackup
|
||||||
|
@ -38,7 +39,6 @@ import java.nio.file.Path
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class ApkBackupRestoreTest : TransportTest() {
|
internal class ApkBackupRestoreTest : TransportTest() {
|
||||||
|
|
||||||
private val pm: PackageManager = mockk()
|
private val pm: PackageManager = mockk()
|
||||||
|
@ -46,16 +46,17 @@ internal class ApkBackupRestoreTest : TransportTest() {
|
||||||
every { packageManager } returns pm
|
every { packageManager } returns pm
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val storagePluginManager: StoragePluginManager = mockk()
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
private val legacyStoragePlugin: LegacyStoragePlugin = mockk()
|
private val legacyStoragePlugin: LegacyStoragePlugin = mockk()
|
||||||
private val storagePlugin: StoragePlugin = mockk()
|
private val storagePlugin: StoragePlugin<*> = mockk()
|
||||||
private val splitCompatChecker: ApkSplitCompatibilityChecker = mockk()
|
private val splitCompatChecker: ApkSplitCompatibilityChecker = mockk()
|
||||||
private val apkInstaller: ApkInstaller = mockk()
|
private val apkInstaller: ApkInstaller = mockk()
|
||||||
|
|
||||||
private val apkBackup = ApkBackup(pm, crypto, settingsManager, metadataManager)
|
private val apkBackup = ApkBackup(pm, crypto, settingsManager, metadataManager)
|
||||||
private val apkRestore: ApkRestore = ApkRestore(
|
private val apkRestore: ApkRestore = ApkRestore(
|
||||||
context = strictContext,
|
context = strictContext,
|
||||||
storagePlugin = storagePlugin,
|
pluginManager = storagePluginManager,
|
||||||
legacyStoragePlugin = legacyStoragePlugin,
|
legacyStoragePlugin = legacyStoragePlugin,
|
||||||
crypto = crypto,
|
crypto = crypto,
|
||||||
splitCompatChecker = splitCompatChecker,
|
splitCompatChecker = splitCompatChecker,
|
||||||
|
@ -90,6 +91,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mockkStatic(PackageUtils::class)
|
mockkStatic(PackageUtils::class)
|
||||||
|
every { storagePluginManager.appPlugin } returns storagePlugin
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -15,6 +15,7 @@ import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.restore.RestorableBackup
|
import com.stevesoltys.seedvault.restore.RestorableBackup
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
|
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
|
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
|
||||||
|
@ -41,7 +42,6 @@ import java.io.IOException
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
internal class ApkRestoreTest : TransportTest() {
|
internal class ApkRestoreTest : TransportTest() {
|
||||||
|
|
||||||
|
@ -49,18 +49,19 @@ internal class ApkRestoreTest : TransportTest() {
|
||||||
private val strictContext: Context = mockk<Context>().apply {
|
private val strictContext: Context = mockk<Context>().apply {
|
||||||
every { packageManager } returns pm
|
every { packageManager } returns pm
|
||||||
}
|
}
|
||||||
private val storagePlugin: StoragePlugin = mockk()
|
private val storagePluginManager: StoragePluginManager = mockk()
|
||||||
|
private val storagePlugin: StoragePlugin<*> = mockk()
|
||||||
private val legacyStoragePlugin: LegacyStoragePlugin = mockk()
|
private val legacyStoragePlugin: LegacyStoragePlugin = mockk()
|
||||||
private val splitCompatChecker: ApkSplitCompatibilityChecker = mockk()
|
private val splitCompatChecker: ApkSplitCompatibilityChecker = mockk()
|
||||||
private val apkInstaller: ApkInstaller = mockk()
|
private val apkInstaller: ApkInstaller = mockk()
|
||||||
|
|
||||||
private val apkRestore: ApkRestore = ApkRestore(
|
private val apkRestore: ApkRestore = ApkRestore(
|
||||||
strictContext,
|
context = strictContext,
|
||||||
storagePlugin,
|
pluginManager = storagePluginManager,
|
||||||
legacyStoragePlugin,
|
legacyStoragePlugin = legacyStoragePlugin,
|
||||||
crypto,
|
crypto = crypto,
|
||||||
splitCompatChecker,
|
splitCompatChecker = splitCompatChecker,
|
||||||
apkInstaller
|
apkInstaller = apkInstaller,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val icon: Drawable = mockk()
|
private val icon: Drawable = mockk()
|
||||||
|
@ -85,6 +86,8 @@ internal class ApkRestoreTest : TransportTest() {
|
||||||
init {
|
init {
|
||||||
// as we don't do strict signature checking, we can use a relaxed mock
|
// as we don't do strict signature checking, we can use a relaxed mock
|
||||||
packageInfo.signingInfo = mockk(relaxed = true)
|
packageInfo.signingInfo = mockk(relaxed = true)
|
||||||
|
|
||||||
|
every { storagePluginManager.appPlugin } returns storagePlugin
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -17,6 +17,7 @@ import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
||||||
import com.stevesoltys.seedvault.transport.backup.FullBackup
|
import com.stevesoltys.seedvault.transport.backup.FullBackup
|
||||||
|
@ -46,7 +47,6 @@ import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class CoordinatorIntegrationTest : TransportTest() {
|
internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
|
|
||||||
private val inputFactory = mockk<InputFactory>()
|
private val inputFactory = mockk<InputFactory>()
|
||||||
|
@ -58,18 +58,20 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
private val metadataReader = MetadataReaderImpl(cryptoImpl)
|
private val metadataReader = MetadataReaderImpl(cryptoImpl)
|
||||||
private val notificationManager = mockk<BackupNotificationManager>()
|
private val notificationManager = mockk<BackupNotificationManager>()
|
||||||
private val dbManager = TestKvDbManager()
|
private val dbManager = TestKvDbManager()
|
||||||
|
private val storagePluginManager: StoragePluginManager = mockk()
|
||||||
|
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
private val legacyPlugin = mockk<LegacyStoragePlugin>()
|
private val legacyPlugin = mockk<LegacyStoragePlugin>()
|
||||||
private val backupPlugin = mockk<StoragePlugin>()
|
private val backupPlugin = mockk<StoragePlugin<*>>()
|
||||||
private val kvBackup =
|
private val kvBackup =
|
||||||
KVBackup(backupPlugin, settingsManager, inputFactory, cryptoImpl, dbManager)
|
KVBackup(storagePluginManager, settingsManager, inputFactory, cryptoImpl, dbManager)
|
||||||
private val fullBackup = FullBackup(backupPlugin, settingsManager, inputFactory, cryptoImpl)
|
private val fullBackup =
|
||||||
|
FullBackup(storagePluginManager, settingsManager, inputFactory, cryptoImpl)
|
||||||
private val apkBackup = mockk<ApkBackup>()
|
private val apkBackup = mockk<ApkBackup>()
|
||||||
private val packageService: PackageService = mockk()
|
private val packageService: PackageService = mockk()
|
||||||
private val backup = BackupCoordinator(
|
private val backup = BackupCoordinator(
|
||||||
context,
|
context,
|
||||||
backupPlugin,
|
storagePluginManager,
|
||||||
kvBackup,
|
kvBackup,
|
||||||
fullBackup,
|
fullBackup,
|
||||||
clock,
|
clock,
|
||||||
|
@ -80,7 +82,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
)
|
)
|
||||||
|
|
||||||
private val kvRestore = KVRestore(
|
private val kvRestore = KVRestore(
|
||||||
backupPlugin,
|
storagePluginManager,
|
||||||
legacyPlugin,
|
legacyPlugin,
|
||||||
outputFactory,
|
outputFactory,
|
||||||
headerReader,
|
headerReader,
|
||||||
|
@ -88,14 +90,14 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
dbManager
|
dbManager
|
||||||
)
|
)
|
||||||
private val fullRestore =
|
private val fullRestore =
|
||||||
FullRestore(backupPlugin, legacyPlugin, outputFactory, headerReader, cryptoImpl)
|
FullRestore(storagePluginManager, legacyPlugin, outputFactory, headerReader, cryptoImpl)
|
||||||
private val restore = RestoreCoordinator(
|
private val restore = RestoreCoordinator(
|
||||||
context,
|
context,
|
||||||
crypto,
|
crypto,
|
||||||
settingsManager,
|
settingsManager,
|
||||||
metadataManager,
|
metadataManager,
|
||||||
notificationManager,
|
notificationManager,
|
||||||
backupPlugin,
|
storagePluginManager,
|
||||||
kvRestore,
|
kvRestore,
|
||||||
fullRestore,
|
fullRestore,
|
||||||
metadataReader
|
metadataReader
|
||||||
|
@ -113,6 +115,10 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
// as we use real crypto, we need a real name for packageInfo
|
// as we use real crypto, we need a real name for packageInfo
|
||||||
private val realName = cryptoImpl.getNameForPackage(salt, packageInfo.packageName)
|
private val realName = cryptoImpl.getNameForPackage(salt, packageInfo.packageName)
|
||||||
|
|
||||||
|
init {
|
||||||
|
every { storagePluginManager.appPlugin } returns backupPlugin
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test key-value backup and restore with 2 records`() = runBlocking {
|
fun `test key-value backup and restore with 2 records`() = runBlocking {
|
||||||
val value = CapturingSlot<ByteArray>()
|
val value = CapturingSlot<ByteArray>()
|
||||||
|
|
|
@ -16,6 +16,7 @@ import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
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.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
||||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
|
@ -33,10 +34,9 @@ import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class BackupCoordinatorTest : BackupTest() {
|
internal class BackupCoordinatorTest : BackupTest() {
|
||||||
|
|
||||||
private val plugin = mockk<StoragePlugin>()
|
private val pluginManager = mockk<StoragePluginManager>()
|
||||||
private val kv = mockk<KVBackup>()
|
private val kv = mockk<KVBackup>()
|
||||||
private val full = mockk<FullBackup>()
|
private val full = mockk<FullBackup>()
|
||||||
private val apkBackup = mockk<ApkBackup>()
|
private val apkBackup = mockk<ApkBackup>()
|
||||||
|
@ -44,27 +44,32 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
private val packageService = mockk<PackageService>()
|
private val packageService = mockk<PackageService>()
|
||||||
|
|
||||||
private val backup = BackupCoordinator(
|
private val backup = BackupCoordinator(
|
||||||
context,
|
context = context,
|
||||||
plugin,
|
pluginManager = pluginManager,
|
||||||
kv,
|
kv = kv,
|
||||||
full,
|
full = full,
|
||||||
clock,
|
clock = clock,
|
||||||
packageService,
|
packageService = packageService,
|
||||||
metadataManager,
|
metadataManager = metadataManager,
|
||||||
settingsManager,
|
settingsManager = settingsManager,
|
||||||
notificationManager
|
nm = notificationManager,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val plugin = mockk<StoragePlugin<*>>()
|
||||||
private val metadataOutputStream = mockk<OutputStream>()
|
private val metadataOutputStream = mockk<OutputStream>()
|
||||||
private val fileDescriptor: ParcelFileDescriptor = mockk()
|
private val fileDescriptor: ParcelFileDescriptor = mockk()
|
||||||
private val packageMetadata: PackageMetadata = mockk()
|
private val packageMetadata: PackageMetadata = mockk()
|
||||||
private val safStorage = SafStorage(
|
private val safStorage = SafStorage(
|
||||||
uri = Uri.EMPTY,
|
config = Uri.EMPTY,
|
||||||
name = getRandomString(),
|
name = getRandomString(),
|
||||||
isUsb = false,
|
isUsb = false,
|
||||||
requiresNetwork = false
|
requiresNetwork = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
every { pluginManager.appPlugin } returns plugin
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `device initialization succeeds and delegates to plugin`() = runBlocking {
|
fun `device initialization succeeds and delegates to plugin`() = runBlocking {
|
||||||
expectStartNewRestoreSet()
|
expectStartNewRestoreSet()
|
||||||
|
@ -90,7 +95,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
expectStartNewRestoreSet()
|
expectStartNewRestoreSet()
|
||||||
coEvery { plugin.initializeDevice() } throws IOException()
|
coEvery { plugin.initializeDevice() } throws IOException()
|
||||||
every { metadataManager.requiresInit } returns maybeTrue
|
every { metadataManager.requiresInit } returns maybeTrue
|
||||||
every { settingsManager.canDoBackupNow() } returns !maybeTrue
|
every { pluginManager.canDoBackupNow() } returns !maybeTrue
|
||||||
every { notificationManager.onBackupError() } just Runs
|
every { notificationManager.onBackupError() } just Runs
|
||||||
|
|
||||||
assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
|
assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
|
||||||
|
@ -109,7 +114,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
expectStartNewRestoreSet()
|
expectStartNewRestoreSet()
|
||||||
coEvery { plugin.initializeDevice() } throws IOException()
|
coEvery { plugin.initializeDevice() } throws IOException()
|
||||||
every { metadataManager.requiresInit } returns false
|
every { metadataManager.requiresInit } returns false
|
||||||
every { settingsManager.canDoBackupNow() } returns false
|
every { pluginManager.canDoBackupNow() } returns false
|
||||||
|
|
||||||
assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
|
assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
|
||||||
|
|
||||||
|
@ -125,7 +130,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
fun `performIncrementalBackup of @pm@ causes re-init when legacy format`() = runBlocking {
|
fun `performIncrementalBackup of @pm@ causes re-init when legacy format`() = runBlocking {
|
||||||
val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
|
val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
|
||||||
|
|
||||||
every { settingsManager.canDoBackupNow() } returns true
|
every { pluginManager.canDoBackupNow() } returns true
|
||||||
every { metadataManager.requiresInit } returns true
|
every { metadataManager.requiresInit } returns true
|
||||||
|
|
||||||
// start new restore set
|
// start new restore set
|
||||||
|
@ -224,7 +229,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
every { kv.getCurrentSize() } returns 42L
|
every { kv.getCurrentSize() } returns 42L
|
||||||
|
|
||||||
coEvery { kv.finishBackup() } returns TRANSPORT_OK
|
coEvery { kv.finishBackup() } returns TRANSPORT_OK
|
||||||
every { settingsManager.canDoBackupNow() } returns false
|
every { pluginManager.canDoBackupNow() } returns false
|
||||||
|
|
||||||
assertEquals(TRANSPORT_OK, backup.finishBackup())
|
assertEquals(TRANSPORT_OK, backup.finishBackup())
|
||||||
}
|
}
|
||||||
|
@ -290,7 +295,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
)
|
)
|
||||||
} just Runs
|
} just Runs
|
||||||
coEvery { full.cancelFullBackup(token, metadata.salt, false) } just Runs
|
coEvery { full.cancelFullBackup(token, metadata.salt, false) } just Runs
|
||||||
every { settingsManager.getSafStorage() } returns safStorage
|
every { pluginManager.storageProperties } returns safStorage
|
||||||
every { settingsManager.useMeteredNetwork } returns false
|
every { settingsManager.useMeteredNetwork } returns false
|
||||||
every { metadataOutputStream.close() } just Runs
|
every { metadataOutputStream.close() } just Runs
|
||||||
|
|
||||||
|
@ -340,7 +345,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
)
|
)
|
||||||
} just Runs
|
} just Runs
|
||||||
coEvery { full.cancelFullBackup(token, metadata.salt, false) } just Runs
|
coEvery { full.cancelFullBackup(token, metadata.salt, false) } just Runs
|
||||||
every { settingsManager.getSafStorage() } returns safStorage
|
every { pluginManager.storageProperties } returns safStorage
|
||||||
every { settingsManager.useMeteredNetwork } returns false
|
every { settingsManager.useMeteredNetwork } returns false
|
||||||
every { metadataOutputStream.close() } just Runs
|
every { metadataOutputStream.close() } just Runs
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
|
||||||
import com.stevesoltys.seedvault.header.VERSION
|
import com.stevesoltys.seedvault.header.VERSION
|
||||||
import com.stevesoltys.seedvault.header.getADForFull
|
import com.stevesoltys.seedvault.header.getADForFull
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
@ -21,16 +22,20 @@ import java.io.FileInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class FullBackupTest : BackupTest() {
|
internal class FullBackupTest : BackupTest() {
|
||||||
|
|
||||||
private val plugin = mockk<StoragePlugin>()
|
private val storagePluginManager: StoragePluginManager = mockk()
|
||||||
private val backup = FullBackup(plugin, settingsManager, inputFactory, crypto)
|
private val plugin = mockk<StoragePlugin<*>>()
|
||||||
|
private val backup = FullBackup(storagePluginManager, settingsManager, inputFactory, crypto)
|
||||||
|
|
||||||
private val bytes = ByteArray(23).apply { Random.nextBytes(this) }
|
private val bytes = ByteArray(23).apply { Random.nextBytes(this) }
|
||||||
private val inputStream = mockk<FileInputStream>()
|
private val inputStream = mockk<FileInputStream>()
|
||||||
private val ad = getADForFull(VERSION, packageInfo.packageName)
|
private val ad = getADForFull(VERSION, packageInfo.packageName)
|
||||||
|
|
||||||
|
init {
|
||||||
|
every { storagePluginManager.appPlugin } returns plugin
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `has no initial state`() {
|
fun `has no initial state`() {
|
||||||
assertFalse(backup.hasState())
|
assertFalse(backup.hasState())
|
||||||
|
|
|
@ -13,6 +13,7 @@ import com.stevesoltys.seedvault.header.MAX_KEY_LENGTH_SIZE
|
||||||
import com.stevesoltys.seedvault.header.VERSION
|
import com.stevesoltys.seedvault.header.VERSION
|
||||||
import com.stevesoltys.seedvault.header.getADForKV
|
import com.stevesoltys.seedvault.header.getADForKV
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import io.mockk.CapturingSlot
|
import io.mockk.CapturingSlot
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
|
@ -30,22 +31,26 @@ import java.io.ByteArrayInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class KVBackupTest : BackupTest() {
|
internal class KVBackupTest : BackupTest() {
|
||||||
|
|
||||||
private val plugin = mockk<StoragePlugin>()
|
private val pluginManager = mockk<StoragePluginManager>()
|
||||||
private val dataInput = mockk<BackupDataInput>()
|
private val dataInput = mockk<BackupDataInput>()
|
||||||
private val dbManager = mockk<KvDbManager>()
|
private val dbManager = mockk<KvDbManager>()
|
||||||
|
|
||||||
private val backup = KVBackup(plugin, settingsManager, inputFactory, crypto, dbManager)
|
private val backup = KVBackup(pluginManager, settingsManager, inputFactory, crypto, dbManager)
|
||||||
|
|
||||||
private val db = mockk<KVDb>()
|
private val db = mockk<KVDb>()
|
||||||
|
private val plugin = mockk<StoragePlugin<*>>()
|
||||||
private val packageName = packageInfo.packageName
|
private val packageName = packageInfo.packageName
|
||||||
private val key = getRandomString(MAX_KEY_LENGTH_SIZE)
|
private val key = getRandomString(MAX_KEY_LENGTH_SIZE)
|
||||||
private val dataValue = Random.nextBytes(23)
|
private val dataValue = Random.nextBytes(23)
|
||||||
private val dbBytes = Random.nextBytes(42)
|
private val dbBytes = Random.nextBytes(42)
|
||||||
private val inputStream = ByteArrayInputStream(dbBytes)
|
private val inputStream = ByteArrayInputStream(dbBytes)
|
||||||
|
|
||||||
|
init {
|
||||||
|
every { pluginManager.appPlugin } returns plugin
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `has no initial state`() {
|
fun `has no initial state`() {
|
||||||
assertFalse(backup.hasState())
|
assertFalse(backup.hasState())
|
||||||
|
@ -231,7 +236,7 @@ internal class KVBackupTest : BackupTest() {
|
||||||
every { dbManager.existsDb(pmPackageInfo.packageName) } returns false
|
every { dbManager.existsDb(pmPackageInfo.packageName) } returns false
|
||||||
every { crypto.getNameForPackage(salt, pmPackageInfo.packageName) } returns name
|
every { crypto.getNameForPackage(salt, pmPackageInfo.packageName) } returns name
|
||||||
every { dbManager.getDb(pmPackageInfo.packageName) } returns db
|
every { dbManager.getDb(pmPackageInfo.packageName) } returns db
|
||||||
every { settingsManager.canDoBackupNow() } returns false
|
every { pluginManager.canDoBackupNow() } returns false
|
||||||
every { db.put(key, dataValue) } just Runs
|
every { db.put(key, dataValue) } just Runs
|
||||||
getDataInput(listOf(true, false))
|
getDataInput(listOf(true, false))
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import com.stevesoltys.seedvault.header.VersionHeader
|
||||||
import com.stevesoltys.seedvault.header.getADForFull
|
import com.stevesoltys.seedvault.header.getADForFull
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import io.mockk.CapturingSlot
|
import io.mockk.CapturingSlot
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
|
@ -31,17 +32,27 @@ import java.io.IOException
|
||||||
import java.security.GeneralSecurityException
|
import java.security.GeneralSecurityException
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class FullRestoreTest : RestoreTest() {
|
internal class FullRestoreTest : RestoreTest() {
|
||||||
|
|
||||||
private val plugin = mockk<StoragePlugin>()
|
private val storagePluginManager: StoragePluginManager = mockk()
|
||||||
|
private val plugin = mockk<StoragePlugin<*>>()
|
||||||
private val legacyPlugin = mockk<LegacyStoragePlugin>()
|
private val legacyPlugin = mockk<LegacyStoragePlugin>()
|
||||||
private val restore = FullRestore(plugin, legacyPlugin, outputFactory, headerReader, crypto)
|
private val restore = FullRestore(
|
||||||
|
pluginManager = storagePluginManager,
|
||||||
|
legacyPlugin = legacyPlugin,
|
||||||
|
outputFactory = outputFactory,
|
||||||
|
headerReader = headerReader,
|
||||||
|
crypto = crypto,
|
||||||
|
)
|
||||||
|
|
||||||
private val encrypted = getRandomByteArray()
|
private val encrypted = getRandomByteArray()
|
||||||
private val outputStream = ByteArrayOutputStream()
|
private val outputStream = ByteArrayOutputStream()
|
||||||
private val ad = getADForFull(VERSION, packageInfo.packageName)
|
private val ad = getADForFull(VERSION, packageInfo.packageName)
|
||||||
|
|
||||||
|
init {
|
||||||
|
every { storagePluginManager.appPlugin } returns plugin
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `has no initial state`() {
|
fun `has no initial state`() {
|
||||||
assertFalse(restore.hasState())
|
assertFalse(restore.hasState())
|
||||||
|
|
|
@ -12,6 +12,7 @@ import com.stevesoltys.seedvault.header.VersionHeader
|
||||||
import com.stevesoltys.seedvault.header.getADForKV
|
import com.stevesoltys.seedvault.header.getADForKV
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.transport.backup.KVDb
|
import com.stevesoltys.seedvault.transport.backup.KVDb
|
||||||
import com.stevesoltys.seedvault.transport.backup.KvDbManager
|
import com.stevesoltys.seedvault.transport.backup.KvDbManager
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
|
@ -33,15 +34,22 @@ import java.security.GeneralSecurityException
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class KVRestoreTest : RestoreTest() {
|
internal class KVRestoreTest : RestoreTest() {
|
||||||
|
|
||||||
private val plugin = mockk<StoragePlugin>()
|
private val storagePluginManager: StoragePluginManager = mockk()
|
||||||
|
private val plugin = mockk<StoragePlugin<*>>()
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
private val legacyPlugin = mockk<LegacyStoragePlugin>()
|
private val legacyPlugin = mockk<LegacyStoragePlugin>()
|
||||||
private val dbManager = mockk<KvDbManager>()
|
private val dbManager = mockk<KvDbManager>()
|
||||||
private val output = mockk<BackupDataOutput>()
|
private val output = mockk<BackupDataOutput>()
|
||||||
private val restore =
|
private val restore = KVRestore(
|
||||||
KVRestore(plugin, legacyPlugin, outputFactory, headerReader, crypto, dbManager)
|
pluginManager = storagePluginManager,
|
||||||
|
legacyPlugin = legacyPlugin,
|
||||||
|
outputFactory = outputFactory,
|
||||||
|
headerReader = headerReader,
|
||||||
|
crypto = crypto,
|
||||||
|
dbManager = dbManager,
|
||||||
|
)
|
||||||
|
|
||||||
private val db = mockk<KVDb>()
|
private val db = mockk<KVDb>()
|
||||||
private val ad = getADForKV(VERSION, packageInfo.packageName)
|
private val ad = getADForKV(VERSION, packageInfo.packageName)
|
||||||
|
@ -60,6 +68,8 @@ internal class KVRestoreTest : RestoreTest() {
|
||||||
init {
|
init {
|
||||||
// for InputStream#readBytes()
|
// for InputStream#readBytes()
|
||||||
mockkStatic("kotlin.io.ByteStreamsKt")
|
mockkStatic("kotlin.io.ByteStreamsKt")
|
||||||
|
|
||||||
|
every { storagePluginManager.appPlugin } returns plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -180,7 +190,6 @@ internal class KVRestoreTest : RestoreTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Suppress("Deprecation")
|
|
||||||
fun `v0 listing records throws`() = runBlocking {
|
fun `v0 listing records throws`() = runBlocking {
|
||||||
restore.initializeState(0x00, token, name, packageInfo)
|
restore.initializeState(0x00, token, name, packageInfo)
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import com.stevesoltys.seedvault.metadata.MetadataReader
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.plugins.EncryptedMetadata
|
import com.stevesoltys.seedvault.plugins.EncryptedMetadata
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
||||||
import com.stevesoltys.seedvault.transport.TransportTest
|
import com.stevesoltys.seedvault.transport.TransportTest
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
|
@ -35,25 +36,25 @@ import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class RestoreCoordinatorTest : TransportTest() {
|
internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
|
|
||||||
private val notificationManager: BackupNotificationManager = mockk()
|
private val notificationManager: BackupNotificationManager = mockk()
|
||||||
private val plugin = mockk<StoragePlugin>()
|
private val storagePluginManager: StoragePluginManager = mockk()
|
||||||
|
private val plugin = mockk<StoragePlugin<*>>()
|
||||||
private val kv = mockk<KVRestore>()
|
private val kv = mockk<KVRestore>()
|
||||||
private val full = mockk<FullRestore>()
|
private val full = mockk<FullRestore>()
|
||||||
private val metadataReader = mockk<MetadataReader>()
|
private val metadataReader = mockk<MetadataReader>()
|
||||||
|
|
||||||
private val restore = RestoreCoordinator(
|
private val restore = RestoreCoordinator(
|
||||||
context,
|
context = context,
|
||||||
crypto,
|
crypto = crypto,
|
||||||
settingsManager,
|
settingsManager = settingsManager,
|
||||||
metadataManager,
|
metadataManager = metadataManager,
|
||||||
notificationManager,
|
notificationManager = notificationManager,
|
||||||
plugin,
|
pluginManager = storagePluginManager,
|
||||||
kv,
|
kv = kv,
|
||||||
full,
|
full = full,
|
||||||
metadataReader
|
metadataReader = metadataReader,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val inputStream = mockk<InputStream>()
|
private val inputStream = mockk<InputStream>()
|
||||||
|
@ -71,6 +72,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
init {
|
init {
|
||||||
metadata.packageMetadataMap[packageInfo2.packageName] =
|
metadata.packageMetadataMap[packageInfo2.packageName] =
|
||||||
PackageMetadata(backupType = BackupType.FULL)
|
PackageMetadata(backupType = BackupType.FULL)
|
||||||
|
|
||||||
|
every { storagePluginManager.appPlugin } returns plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -164,7 +167,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `startRestore() optimized auto-restore with removed storage shows notification`() =
|
fun `startRestore() optimized auto-restore with removed storage shows notification`() =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
every { settingsManager.getSafStorage() } returns safStorage
|
every { storagePluginManager.storageProperties } returns safStorage
|
||||||
every { safStorage.isUnavailableUsb(context) } returns true
|
every { safStorage.isUnavailableUsb(context) } returns true
|
||||||
every { metadataManager.getPackageMetadata(packageName) } returns PackageMetadata(42L)
|
every { metadataManager.getPackageMetadata(packageName) } returns PackageMetadata(42L)
|
||||||
every { safStorage.name } returns storageName
|
every { safStorage.name } returns storageName
|
||||||
|
@ -188,7 +191,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `startRestore() optimized auto-restore with available storage shows no notification`() =
|
fun `startRestore() optimized auto-restore with available storage shows no notification`() =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
every { settingsManager.getSafStorage() } returns safStorage
|
every { storagePluginManager.storageProperties } returns safStorage
|
||||||
every { safStorage.isUnavailableUsb(context) } returns false
|
every { safStorage.isUnavailableUsb(context) } returns false
|
||||||
|
|
||||||
restore.beforeStartRestore(metadata)
|
restore.beforeStartRestore(metadata)
|
||||||
|
@ -204,7 +207,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `startRestore() with removed storage shows no notification`() = runBlocking {
|
fun `startRestore() with removed storage shows no notification`() = runBlocking {
|
||||||
every { settingsManager.getSafStorage() } returns safStorage
|
every { storagePluginManager.storageProperties } returns safStorage
|
||||||
every { safStorage.isUnavailableUsb(context) } returns true
|
every { safStorage.isUnavailableUsb(context) } returns true
|
||||||
every { metadataManager.getPackageMetadata(packageName) } returns null
|
every { metadataManager.getPackageMetadata(packageName) } returns null
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import com.stevesoltys.seedvault.header.HeaderReaderImpl
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
|
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
|
||||||
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.toByteArrayFromHex
|
import com.stevesoltys.seedvault.toByteArrayFromHex
|
||||||
import com.stevesoltys.seedvault.transport.TransportTest
|
import com.stevesoltys.seedvault.transport.TransportTest
|
||||||
import com.stevesoltys.seedvault.transport.backup.KvDbManager
|
import com.stevesoltys.seedvault.transport.backup.KvDbManager
|
||||||
|
@ -35,7 +36,6 @@ import javax.crypto.spec.SecretKeySpec
|
||||||
/**
|
/**
|
||||||
* Tests that we can still restore Version 0 backups with current code.
|
* Tests that we can still restore Version 0 backups with current code.
|
||||||
*/
|
*/
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class RestoreV0IntegrationTest : TransportTest() {
|
internal class RestoreV0IntegrationTest : TransportTest() {
|
||||||
|
|
||||||
private val outputFactory = mockk<OutputFactory>()
|
private val outputFactory = mockk<OutputFactory>()
|
||||||
|
@ -49,30 +49,31 @@ internal class RestoreV0IntegrationTest : TransportTest() {
|
||||||
private val dbManager = mockk<KvDbManager>()
|
private val dbManager = mockk<KvDbManager>()
|
||||||
private val metadataReader = MetadataReaderImpl(cryptoImpl)
|
private val metadataReader = MetadataReaderImpl(cryptoImpl)
|
||||||
private val notificationManager = mockk<BackupNotificationManager>()
|
private val notificationManager = mockk<BackupNotificationManager>()
|
||||||
|
private val storagePluginManager: StoragePluginManager = mockk()
|
||||||
|
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
private val legacyPlugin = mockk<LegacyStoragePlugin>()
|
private val legacyPlugin = mockk<LegacyStoragePlugin>()
|
||||||
private val backupPlugin = mockk<StoragePlugin>()
|
private val backupPlugin = mockk<StoragePlugin<*>>()
|
||||||
private val kvRestore = KVRestore(
|
private val kvRestore = KVRestore(
|
||||||
backupPlugin,
|
pluginManager = storagePluginManager,
|
||||||
legacyPlugin,
|
legacyPlugin = legacyPlugin,
|
||||||
outputFactory,
|
outputFactory = outputFactory,
|
||||||
headerReader,
|
headerReader = headerReader,
|
||||||
cryptoImpl,
|
crypto = cryptoImpl,
|
||||||
dbManager
|
dbManager = dbManager,
|
||||||
)
|
)
|
||||||
private val fullRestore =
|
private val fullRestore =
|
||||||
FullRestore(backupPlugin, legacyPlugin, outputFactory, headerReader, cryptoImpl)
|
FullRestore(storagePluginManager, legacyPlugin, outputFactory, headerReader, cryptoImpl)
|
||||||
private val restore = RestoreCoordinator(
|
private val restore = RestoreCoordinator(
|
||||||
context,
|
context = context,
|
||||||
crypto,
|
crypto = crypto,
|
||||||
settingsManager,
|
settingsManager = settingsManager,
|
||||||
metadataManager,
|
metadataManager = metadataManager,
|
||||||
notificationManager,
|
notificationManager = notificationManager,
|
||||||
backupPlugin,
|
pluginManager = storagePluginManager,
|
||||||
kvRestore,
|
kv = kvRestore,
|
||||||
fullRestore,
|
full = fullRestore,
|
||||||
metadataReader
|
metadataReader = metadataReader,
|
||||||
).apply { beforeStartRestore(metadata.copy(version = 0x00)) }
|
).apply { beforeStartRestore(metadata.copy(version = 0x00)) }
|
||||||
|
|
||||||
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
|
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
|
||||||
|
@ -116,6 +117,10 @@ internal class RestoreV0IntegrationTest : TransportTest() {
|
||||||
private val key2 = "RestoreKey2"
|
private val key2 = "RestoreKey2"
|
||||||
private val key264 = key2.encodeBase64()
|
private val key264 = key2.encodeBase64()
|
||||||
|
|
||||||
|
init {
|
||||||
|
every { storagePluginManager.appPlugin } returns backupPlugin
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test key-value backup and restore with 2 records`() = runBlocking {
|
fun `test key-value backup and restore with 2 records`() = runBlocking {
|
||||||
val encryptedAppData = ("00002A2C701AA7C91D1286E265D29169B25C41E6D0" +
|
val encryptedAppData = ("00002A2C701AA7C91D1286E265D29169B25C41E6D0" +
|
||||||
|
|
|
@ -10,6 +10,7 @@ import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
import com.stevesoltys.seedvault.plugins.saf.FILE_BACKUP_METADATA
|
||||||
import com.stevesoltys.seedvault.transport.TransportTest
|
import com.stevesoltys.seedvault.transport.TransportTest
|
||||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||||
|
@ -32,7 +33,8 @@ internal class ApkBackupManagerTest : TransportTest() {
|
||||||
|
|
||||||
private val packageService: PackageService = mockk()
|
private val packageService: PackageService = mockk()
|
||||||
private val apkBackup: ApkBackup = mockk()
|
private val apkBackup: ApkBackup = mockk()
|
||||||
private val plugin: StoragePlugin = mockk()
|
private val storagePluginManager: StoragePluginManager = mockk()
|
||||||
|
private val plugin: StoragePlugin<*> = mockk()
|
||||||
private val nm: BackupNotificationManager = mockk()
|
private val nm: BackupNotificationManager = mockk()
|
||||||
|
|
||||||
private val apkBackupManager = ApkBackupManager(
|
private val apkBackupManager = ApkBackupManager(
|
||||||
|
@ -41,13 +43,17 @@ internal class ApkBackupManagerTest : TransportTest() {
|
||||||
metadataManager = metadataManager,
|
metadataManager = metadataManager,
|
||||||
packageService = packageService,
|
packageService = packageService,
|
||||||
apkBackup = apkBackup,
|
apkBackup = apkBackup,
|
||||||
plugin = plugin,
|
pluginManager = storagePluginManager,
|
||||||
nm = nm,
|
nm = nm,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val metadataOutputStream = mockk<OutputStream>()
|
private val metadataOutputStream = mockk<OutputStream>()
|
||||||
private val packageMetadata: PackageMetadata = mockk()
|
private val packageMetadata: PackageMetadata = mockk()
|
||||||
|
|
||||||
|
init {
|
||||||
|
every { storagePluginManager.appPlugin } returns plugin
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Package state of app that is not stopped gets recorded as not-allowed`() = runBlocking {
|
fun `Package state of app that is not stopped gets recorded as not-allowed`() = runBlocking {
|
||||||
every { nm.onAppsNotBackedUp() } just Runs
|
every { nm.onAppsNotBackedUp() } just Runs
|
||||||
|
|
|
@ -38,7 +38,6 @@ import java.io.OutputStream
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class ApkBackupTest : BackupTest() {
|
internal class ApkBackupTest : BackupTest() {
|
||||||
|
|
||||||
private val pm: PackageManager = mockk()
|
private val pm: PackageManager = mockk()
|
||||||
|
|
|
@ -18,7 +18,7 @@ class App : Application() {
|
||||||
val settingsManager: SettingsManager by lazy { SettingsManager(applicationContext) }
|
val settingsManager: SettingsManager by lazy { SettingsManager(applicationContext) }
|
||||||
val storageBackup: StorageBackup by lazy {
|
val storageBackup: StorageBackup by lazy {
|
||||||
val plugin = TestSafStoragePlugin(this) { settingsManager.getBackupLocation() }
|
val plugin = TestSafStoragePlugin(this) { settingsManager.getBackupLocation() }
|
||||||
StorageBackup(this, plugin)
|
StorageBackup(this, { plugin })
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
|
|
@ -36,10 +36,9 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
private const val TAG = "StorageBackup"
|
private const val TAG = "StorageBackup"
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
public class StorageBackup(
|
public class StorageBackup(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val plugin: StoragePlugin,
|
private val pluginGetter: () -> StoragePlugin,
|
||||||
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -50,18 +49,18 @@ public class StorageBackup(
|
||||||
private val uriStore by lazy { db.getUriStore() }
|
private val uriStore by lazy { db.getUriStore() }
|
||||||
|
|
||||||
private val mediaScanner by lazy { MediaScanner(context) }
|
private val mediaScanner by lazy { MediaScanner(context) }
|
||||||
private val snapshotRetriever = SnapshotRetriever(plugin)
|
private val snapshotRetriever = SnapshotRetriever(pluginGetter)
|
||||||
private val chunksCacheRepopulater = ChunksCacheRepopulater(db, plugin, snapshotRetriever)
|
private val chunksCacheRepopulater = ChunksCacheRepopulater(db, pluginGetter, snapshotRetriever)
|
||||||
private val backup by lazy {
|
private val backup by lazy {
|
||||||
val documentScanner = DocumentScanner(context)
|
val documentScanner = DocumentScanner(context)
|
||||||
val fileScanner = FileScanner(uriStore, mediaScanner, documentScanner)
|
val fileScanner = FileScanner(uriStore, mediaScanner, documentScanner)
|
||||||
Backup(context, db, fileScanner, plugin, chunksCacheRepopulater)
|
Backup(context, db, fileScanner, pluginGetter, chunksCacheRepopulater)
|
||||||
}
|
}
|
||||||
private val restore by lazy {
|
private val restore by lazy {
|
||||||
Restore(context, plugin, snapshotRetriever, FileRestore(context, mediaScanner))
|
Restore(context, pluginGetter, snapshotRetriever, FileRestore(context, mediaScanner))
|
||||||
}
|
}
|
||||||
private val retention = RetentionManager(context)
|
private val retention = RetentionManager(context)
|
||||||
private val pruner by lazy { Pruner(db, retention, plugin, snapshotRetriever) }
|
private val pruner by lazy { Pruner(db, retention, pluginGetter, snapshotRetriever) }
|
||||||
|
|
||||||
private val backupRunning = AtomicBoolean(false)
|
private val backupRunning = AtomicBoolean(false)
|
||||||
private val restoreRunning = AtomicBoolean(false)
|
private val restoreRunning = AtomicBoolean(false)
|
||||||
|
@ -109,7 +108,7 @@ public class StorageBackup(
|
||||||
* (see [deleteAllSnapshots]) as well as clears local cache (see [clearCache]).
|
* (see [deleteAllSnapshots]) as well as clears local cache (see [clearCache]).
|
||||||
*/
|
*/
|
||||||
public suspend fun init() {
|
public suspend fun init() {
|
||||||
plugin.init()
|
pluginGetter().init()
|
||||||
deleteAllSnapshots()
|
deleteAllSnapshots()
|
||||||
clearCache()
|
clearCache()
|
||||||
}
|
}
|
||||||
|
@ -123,9 +122,9 @@ public class StorageBackup(
|
||||||
*/
|
*/
|
||||||
public suspend fun deleteAllSnapshots(): Unit = withContext(dispatcher) {
|
public suspend fun deleteAllSnapshots(): Unit = withContext(dispatcher) {
|
||||||
try {
|
try {
|
||||||
plugin.getCurrentBackupSnapshots().forEach {
|
pluginGetter().getCurrentBackupSnapshots().forEach {
|
||||||
try {
|
try {
|
||||||
plugin.deleteBackupSnapshot(it)
|
pluginGetter().deleteBackupSnapshot(it)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error deleting snapshot $it", e)
|
Log.e(TAG, "Error deleting snapshot $it", e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,12 +35,11 @@ internal class BackupResult(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class Backup(
|
internal class Backup(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val db: Db,
|
private val db: Db,
|
||||||
private val fileScanner: FileScanner,
|
private val fileScanner: FileScanner,
|
||||||
private val storagePlugin: StoragePlugin,
|
private val storagePluginGetter: () -> StoragePlugin,
|
||||||
private val cacheRepopulater: ChunksCacheRepopulater,
|
private val cacheRepopulater: ChunksCacheRepopulater,
|
||||||
chunkSizeMax: Int = CHUNK_SIZE_MAX,
|
chunkSizeMax: Int = CHUNK_SIZE_MAX,
|
||||||
private val streamCrypto: StreamCrypto = StreamCrypto,
|
private val streamCrypto: StreamCrypto = StreamCrypto,
|
||||||
|
@ -54,6 +53,7 @@ internal class Backup(
|
||||||
}
|
}
|
||||||
|
|
||||||
private val contentResolver = context.contentResolver
|
private val contentResolver = context.contentResolver
|
||||||
|
private val storagePlugin get() = storagePluginGetter()
|
||||||
private val filesCache = db.getFilesCache()
|
private val filesCache = db.getFilesCache()
|
||||||
private val chunksCache = db.getChunksCache()
|
private val chunksCache = db.getChunksCache()
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,9 @@ import kotlin.time.toDuration
|
||||||
|
|
||||||
private const val TAG = "ChunksCacheRepopulater"
|
private const val TAG = "ChunksCacheRepopulater"
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class ChunksCacheRepopulater(
|
internal class ChunksCacheRepopulater(
|
||||||
private val db: Db,
|
private val db: Db,
|
||||||
private val storagePlugin: StoragePlugin,
|
private val storagePlugin: () -> StoragePlugin,
|
||||||
private val snapshotRetriever: SnapshotRetriever,
|
private val snapshotRetriever: SnapshotRetriever,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -43,7 +42,7 @@ internal class ChunksCacheRepopulater(
|
||||||
availableChunkIds: HashSet<String>,
|
availableChunkIds: HashSet<String>,
|
||||||
) {
|
) {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
val snapshots = storagePlugin.getCurrentBackupSnapshots().mapNotNull { storedSnapshot ->
|
val snapshots = storagePlugin().getCurrentBackupSnapshots().mapNotNull { storedSnapshot ->
|
||||||
try {
|
try {
|
||||||
snapshotRetriever.getSnapshot(streamKey, storedSnapshot)
|
snapshotRetriever.getSnapshot(streamKey, storedSnapshot)
|
||||||
} catch (e: GeneralSecurityException) {
|
} catch (e: GeneralSecurityException) {
|
||||||
|
@ -63,7 +62,7 @@ internal class ChunksCacheRepopulater(
|
||||||
// delete chunks that are not references by any snapshot anymore
|
// delete chunks that are not references by any snapshot anymore
|
||||||
val chunksToDelete = availableChunkIds.subtract(cachedChunks.map { it.id })
|
val chunksToDelete = availableChunkIds.subtract(cachedChunks.map { it.id })
|
||||||
val deletionDuration = measure {
|
val deletionDuration = measure {
|
||||||
storagePlugin.deleteChunks(chunksToDelete.toList())
|
storagePlugin().deleteChunks(chunksToDelete.toList())
|
||||||
}
|
}
|
||||||
Log.i(TAG, "Deleting ${chunksToDelete.size} chunks took $deletionDuration")
|
Log.i(TAG, "Deleting ${chunksToDelete.size} chunks took $deletionDuration")
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,8 @@ import org.calyxos.backup.storage.restore.readVersion
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.GeneralSecurityException
|
import java.security.GeneralSecurityException
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class SnapshotRetriever(
|
internal class SnapshotRetriever(
|
||||||
private val storagePlugin: StoragePlugin,
|
private val storagePlugin: () -> StoragePlugin,
|
||||||
private val streamCrypto: StreamCrypto = StreamCrypto,
|
private val streamCrypto: StreamCrypto = StreamCrypto,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -26,7 +25,7 @@ internal class SnapshotRetriever(
|
||||||
InvalidProtocolBufferException::class,
|
InvalidProtocolBufferException::class,
|
||||||
)
|
)
|
||||||
suspend fun getSnapshot(streamKey: ByteArray, storedSnapshot: StoredSnapshot): BackupSnapshot {
|
suspend fun getSnapshot(streamKey: ByteArray, storedSnapshot: StoredSnapshot): BackupSnapshot {
|
||||||
return storagePlugin.getBackupSnapshotInputStream(storedSnapshot).use { inputStream ->
|
return storagePlugin().getBackupSnapshotInputStream(storedSnapshot).use { inputStream ->
|
||||||
val version = inputStream.readVersion()
|
val version = inputStream.readVersion()
|
||||||
val timestamp = storedSnapshot.timestamp
|
val timestamp = storedSnapshot.timestamp
|
||||||
val ad = streamCrypto.getAssociatedDataForSnapshot(timestamp, version.toByte())
|
val ad = streamCrypto.getAssociatedDataForSnapshot(timestamp, version.toByte())
|
||||||
|
|
|
@ -19,15 +19,15 @@ import kotlin.time.ExperimentalTime
|
||||||
|
|
||||||
private val TAG = Pruner::class.java.simpleName
|
private val TAG = Pruner::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class Pruner(
|
internal class Pruner(
|
||||||
private val db: Db,
|
private val db: Db,
|
||||||
private val retentionManager: RetentionManager,
|
private val retentionManager: RetentionManager,
|
||||||
private val storagePlugin: StoragePlugin,
|
private val storagePluginGetter: () -> StoragePlugin,
|
||||||
private val snapshotRetriever: SnapshotRetriever,
|
private val snapshotRetriever: SnapshotRetriever,
|
||||||
streamCrypto: StreamCrypto = StreamCrypto,
|
streamCrypto: StreamCrypto = StreamCrypto,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val storagePlugin get() = storagePluginGetter()
|
||||||
private val chunksCache = db.getChunksCache()
|
private val chunksCache = db.getChunksCache()
|
||||||
private val streamKey = try {
|
private val streamKey = try {
|
||||||
streamCrypto.deriveStreamKey(storagePlugin.getMasterKey())
|
streamCrypto.deriveStreamKey(storagePlugin.getMasterKey())
|
||||||
|
|
|
@ -14,14 +14,15 @@ import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.GeneralSecurityException
|
import java.security.GeneralSecurityException
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal abstract class AbstractChunkRestore(
|
internal abstract class AbstractChunkRestore(
|
||||||
private val storagePlugin: StoragePlugin,
|
private val storagePluginGetter: () -> StoragePlugin,
|
||||||
private val fileRestore: FileRestore,
|
private val fileRestore: FileRestore,
|
||||||
private val streamCrypto: StreamCrypto,
|
private val streamCrypto: StreamCrypto,
|
||||||
private val streamKey: ByteArray,
|
private val streamKey: ByteArray,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val storagePlugin get() = storagePluginGetter()
|
||||||
|
|
||||||
@Throws(IOException::class, GeneralSecurityException::class)
|
@Throws(IOException::class, GeneralSecurityException::class)
|
||||||
protected suspend fun getAndDecryptChunk(
|
protected suspend fun getAndDecryptChunk(
|
||||||
version: Int,
|
version: Int,
|
||||||
|
|
|
@ -24,7 +24,7 @@ private const val TAG = "MultiChunkRestore"
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class MultiChunkRestore(
|
internal class MultiChunkRestore(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
storagePlugin: StoragePlugin,
|
storagePlugin: () -> StoragePlugin,
|
||||||
fileRestore: FileRestore,
|
fileRestore: FileRestore,
|
||||||
streamCrypto: StreamCrypto,
|
streamCrypto: StreamCrypto,
|
||||||
streamKey: ByteArray,
|
streamKey: ByteArray,
|
||||||
|
|
|
@ -28,12 +28,13 @@ private const val TAG = "Restore"
|
||||||
|
|
||||||
internal class Restore(
|
internal class Restore(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val storagePlugin: StoragePlugin,
|
private val storagePluginGetter: () -> StoragePlugin,
|
||||||
private val snapshotRetriever: SnapshotRetriever,
|
private val snapshotRetriever: SnapshotRetriever,
|
||||||
fileRestore: FileRestore,
|
fileRestore: FileRestore,
|
||||||
streamCrypto: StreamCrypto = StreamCrypto,
|
streamCrypto: StreamCrypto = StreamCrypto,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val storagePlugin get() = storagePluginGetter()
|
||||||
private val streamKey by lazy {
|
private val streamKey by lazy {
|
||||||
// This class might get instantiated before the StoragePlugin had time to provide the key
|
// This class might get instantiated before the StoragePlugin had time to provide the key
|
||||||
// so we need to get it lazily here to prevent crashes. We can still crash later,
|
// so we need to get it lazily here to prevent crashes. We can still crash later,
|
||||||
|
@ -47,13 +48,13 @@ internal class Restore(
|
||||||
|
|
||||||
// lazily instantiate these, so they don't try to get the streamKey too early
|
// lazily instantiate these, so they don't try to get the streamKey too early
|
||||||
private val zipChunkRestore by lazy {
|
private val zipChunkRestore by lazy {
|
||||||
ZipChunkRestore(storagePlugin, fileRestore, streamCrypto, streamKey)
|
ZipChunkRestore(storagePluginGetter, fileRestore, streamCrypto, streamKey)
|
||||||
}
|
}
|
||||||
private val singleChunkRestore by lazy {
|
private val singleChunkRestore by lazy {
|
||||||
SingleChunkRestore(storagePlugin, fileRestore, streamCrypto, streamKey)
|
SingleChunkRestore(storagePluginGetter, fileRestore, streamCrypto, streamKey)
|
||||||
}
|
}
|
||||||
private val multiChunkRestore by lazy {
|
private val multiChunkRestore by lazy {
|
||||||
MultiChunkRestore(context, storagePlugin, fileRestore, streamCrypto, streamKey)
|
MultiChunkRestore(context, storagePluginGetter, fileRestore, streamCrypto, streamKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBackupSnapshots(): Flow<SnapshotResult> = flow {
|
fun getBackupSnapshots(): Flow<SnapshotResult> = flow {
|
||||||
|
|
|
@ -13,9 +13,8 @@ import org.calyxos.backup.storage.crypto.StreamCrypto
|
||||||
|
|
||||||
private const val TAG = "SingleChunkRestore"
|
private const val TAG = "SingleChunkRestore"
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class SingleChunkRestore(
|
internal class SingleChunkRestore(
|
||||||
storagePlugin: StoragePlugin,
|
storagePlugin: () -> StoragePlugin,
|
||||||
fileRestore: FileRestore,
|
fileRestore: FileRestore,
|
||||||
streamCrypto: StreamCrypto,
|
streamCrypto: StreamCrypto,
|
||||||
streamKey: ByteArray,
|
streamKey: ByteArray,
|
||||||
|
|
|
@ -17,9 +17,8 @@ import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
private const val TAG = "ZipChunkRestore"
|
private const val TAG = "ZipChunkRestore"
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class ZipChunkRestore(
|
internal class ZipChunkRestore(
|
||||||
storagePlugin: StoragePlugin,
|
storagePlugin: () -> StoragePlugin,
|
||||||
fileRestore: FileRestore,
|
fileRestore: FileRestore,
|
||||||
streamCrypto: StreamCrypto,
|
streamCrypto: StreamCrypto,
|
||||||
streamKey: ByteArray,
|
streamKey: ByteArray,
|
||||||
|
|
|
@ -58,7 +58,6 @@ import java.io.OutputStream
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class BackupRestoreTest {
|
internal class BackupRestoreTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
|
@ -71,9 +70,10 @@ internal class BackupRestoreTest {
|
||||||
private val contentResolver: ContentResolver = mockk()
|
private val contentResolver: ContentResolver = mockk()
|
||||||
|
|
||||||
private val fileScanner: FileScanner = mockk()
|
private val fileScanner: FileScanner = mockk()
|
||||||
|
private val pluginGetter: () -> StoragePlugin = mockk()
|
||||||
private val plugin: StoragePlugin = mockk()
|
private val plugin: StoragePlugin = mockk()
|
||||||
private val fileRestore: FileRestore = mockk()
|
private val fileRestore: FileRestore = mockk()
|
||||||
private val snapshotRetriever = SnapshotRetriever(plugin)
|
private val snapshotRetriever = SnapshotRetriever(pluginGetter)
|
||||||
private val cacheRepopulater: ChunksCacheRepopulater = mockk()
|
private val cacheRepopulater: ChunksCacheRepopulater = mockk()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -84,6 +84,7 @@ internal class BackupRestoreTest {
|
||||||
|
|
||||||
mockkStatic("org.calyxos.backup.storage.UriUtilsKt")
|
mockkStatic("org.calyxos.backup.storage.UriUtilsKt")
|
||||||
|
|
||||||
|
every { pluginGetter() } returns plugin
|
||||||
every { db.getFilesCache() } returns filesCache
|
every { db.getFilesCache() } returns filesCache
|
||||||
every { db.getChunksCache() } returns chunksCache
|
every { db.getChunksCache() } returns chunksCache
|
||||||
every { plugin.getMasterKey() } returns SecretKeySpec(
|
every { plugin.getMasterKey() } returns SecretKeySpec(
|
||||||
|
@ -94,11 +95,11 @@ internal class BackupRestoreTest {
|
||||||
every { context.contentResolver } returns contentResolver
|
every { context.contentResolver } returns contentResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
private val restore = Restore(context, plugin, snapshotRetriever, fileRestore)
|
private val restore = Restore(context, pluginGetter, snapshotRetriever, fileRestore)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testZipAndSingleRandom(): Unit = runBlocking {
|
fun testZipAndSingleRandom(): Unit = runBlocking {
|
||||||
val backup = Backup(context, db, fileScanner, plugin, cacheRepopulater)
|
val backup = Backup(context, db, fileScanner, pluginGetter, cacheRepopulater)
|
||||||
|
|
||||||
val smallFileMBytes = Random.nextBytes(Random.nextInt(SMALL_FILE_SIZE_MAX))
|
val smallFileMBytes = Random.nextBytes(Random.nextInt(SMALL_FILE_SIZE_MAX))
|
||||||
val smallFileM = getRandomMediaFile(smallFileMBytes.size)
|
val smallFileM = getRandomMediaFile(smallFileMBytes.size)
|
||||||
|
@ -235,7 +236,7 @@ internal class BackupRestoreTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMultiChunks(): Unit = runBlocking {
|
fun testMultiChunks(): Unit = runBlocking {
|
||||||
val backup = Backup(context, db, fileScanner, plugin, cacheRepopulater, 4)
|
val backup = Backup(context, db, fileScanner, pluginGetter, cacheRepopulater, 4)
|
||||||
|
|
||||||
val chunk1 = byteArrayOf(0x00, 0x01, 0x02, 0x03)
|
val chunk1 = byteArrayOf(0x00, 0x01, 0x02, 0x03)
|
||||||
val chunk2 = byteArrayOf(0x04, 0x05, 0x06, 0x07)
|
val chunk2 = byteArrayOf(0x04, 0x05, 0x06, 0x07)
|
||||||
|
|
|
@ -26,18 +26,19 @@ import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class ChunksCacheRepopulaterTest {
|
internal class ChunksCacheRepopulaterTest {
|
||||||
|
|
||||||
private val db: Db = mockk()
|
private val db: Db = mockk()
|
||||||
private val chunksCache: ChunksCache = mockk()
|
private val chunksCache: ChunksCache = mockk()
|
||||||
|
private val pluginGetter: () -> StoragePlugin = mockk()
|
||||||
private val plugin: StoragePlugin = mockk()
|
private val plugin: StoragePlugin = mockk()
|
||||||
private val snapshotRetriever: SnapshotRetriever = mockk()
|
private val snapshotRetriever: SnapshotRetriever = mockk()
|
||||||
private val streamKey = "This is a backup key for testing".toByteArray()
|
private val streamKey = "This is a backup key for testing".toByteArray()
|
||||||
private val cacheRepopulater = ChunksCacheRepopulater(db, plugin, snapshotRetriever)
|
private val cacheRepopulater = ChunksCacheRepopulater(db, pluginGetter, snapshotRetriever)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mockLog()
|
mockLog()
|
||||||
|
every { pluginGetter() } returns plugin
|
||||||
every { db.getChunksCache() } returns chunksCache
|
every { db.getChunksCache() } returns chunksCache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,11 +32,11 @@ import org.junit.Test
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
internal class PrunerTest {
|
internal class PrunerTest {
|
||||||
|
|
||||||
private val db: Db = mockk()
|
private val db: Db = mockk()
|
||||||
private val chunksCache: ChunksCache = mockk()
|
private val chunksCache: ChunksCache = mockk()
|
||||||
|
private val pluginGetter: () -> StoragePlugin = mockk()
|
||||||
private val plugin: StoragePlugin = mockk()
|
private val plugin: StoragePlugin = mockk()
|
||||||
private val snapshotRetriever: SnapshotRetriever = mockk()
|
private val snapshotRetriever: SnapshotRetriever = mockk()
|
||||||
private val retentionManager: RetentionManager = mockk()
|
private val retentionManager: RetentionManager = mockk()
|
||||||
|
@ -46,12 +46,13 @@ internal class PrunerTest {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mockLog(false)
|
mockLog(false)
|
||||||
|
every { pluginGetter() } returns plugin
|
||||||
every { db.getChunksCache() } returns chunksCache
|
every { db.getChunksCache() } returns chunksCache
|
||||||
every { plugin.getMasterKey() } returns masterKey
|
every { plugin.getMasterKey() } returns masterKey
|
||||||
every { streamCrypto.deriveStreamKey(masterKey) } returns streamKey
|
every { streamCrypto.deriveStreamKey(masterKey) } returns streamKey
|
||||||
}
|
}
|
||||||
|
|
||||||
private val pruner = Pruner(db, retentionManager, plugin, snapshotRetriever, streamCrypto)
|
private val pruner = Pruner(db, retentionManager, pluginGetter, snapshotRetriever, streamCrypto)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test() = runBlocking {
|
fun test() = runBlocking {
|
||||||
|
|
Loading…
Reference in a new issue