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:
Torsten Grote 2024-04-12 17:47:08 -03:00
parent 2489190824
commit 7e612cb8e0
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
70 changed files with 720 additions and 502 deletions

View file

@ -41,19 +41,20 @@ class KoinInstrumentationTestApp : App() {
viewModel {
currentRestoreViewModel =
spyk(RestoreViewModel(context, get(), get(), get(), get(), get(), get()))
spyk(RestoreViewModel(context, get(), get(), get(), get(), get(), get(), get()))
currentRestoreViewModel!!
}
viewModel {
currentBackupStorageViewModel =
spyk(BackupStorageViewModel(context, get(), get(), get(), get()))
val viewModel =
BackupStorageViewModel(context, get(), get(), get(), get(), get(), get(), get())
currentBackupStorageViewModel = spyk(viewModel)
currentBackupStorageViewModel!!
}
viewModel {
currentRestoreStorageViewModel =
spyk(RestoreStorageViewModel(context, get(), get()))
spyk(RestoreStorageViewModel(context, get(), get(), get(), get()))
currentRestoreStorageViewModel!!
}
}

View file

@ -1,5 +1,6 @@
package com.stevesoltys.seedvault
import android.net.Uri
import androidx.test.core.content.pm.PackageInfoBuilder
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@ -28,20 +29,24 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@RunWith(AndroidJUnit4::class)
@Suppress("BlockingMethodInNonBlockingContext")
@MediumTest
class PluginTest : KoinComponent {
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val settingsManager: SettingsManager by inject()
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")
private val legacyStoragePlugin: LegacyStoragePlugin =
DocumentsProviderLegacyPlugin(context, storage)
private val legacyStoragePlugin: LegacyStoragePlugin = DocumentsProviderLegacyPlugin(context) {
storage
}
private val token = System.currentTimeMillis() - 365L * 24L * 60L * 60L * 1000L
private val packageInfo = PackageInfoBuilder.newBuilder().setPackageName("org.example").build()
@ -76,8 +81,6 @@ class PluginTest : KoinComponent {
fun testInitializationAndRestoreSets() = runBlocking(Dispatchers.IO) {
// no backups available initially
assertEquals(0, storagePlugin.getAvailableBackups()?.toList()?.size)
val s = settingsManager.getSafStorage() ?: error("no storage")
assertFalse(storagePlugin.hasBackup(s))
// prepare returned tokens requested when initializing device
every { mockedSettingsManager.getToken() } returnsMany listOf(token, token + 1, token + 1)
@ -92,7 +95,6 @@ class PluginTest : KoinComponent {
// one backup available now
assertEquals(1, storagePlugin.getAvailableBackups()?.toList()?.size)
assertTrue(storagePlugin.hasBackup(s))
// initializing again (with another restore set) does add a restore set
storagePlugin.startNewRestoreSet(token + 1)
@ -100,7 +102,6 @@ class PluginTest : KoinComponent {
storagePlugin.getOutputStream(token + 1, FILE_BACKUP_METADATA)
.writeAndClose(getRandomByteArray())
assertEquals(2, storagePlugin.getAvailableBackups()?.toList()?.size)
assertTrue(storagePlugin.hasBackup(s))
// initializing again (without new restore set) doesn't change number of restore sets
storagePlugin.initializeDevice()

View file

@ -39,13 +39,16 @@ import java.io.IOException
import kotlin.random.Random
@RunWith(AndroidJUnit4::class)
@Suppress("BlockingMethodInNonBlockingContext")
@MediumTest
class DocumentsStorageTest : KoinComponent {
private val context = InstrumentationRegistry.getInstrumentation().targetContext
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 lateinit var file: DocumentFile

View file

@ -24,7 +24,7 @@ class PackageServiceTest : KoinComponent {
private val settingsManager: SettingsManager by inject()
private val storagePlugin: StoragePlugin by inject()
private val storagePlugin: StoragePlugin<*> by inject()
@Test
fun testNotAllowedPackages() {

View file

@ -13,13 +13,14 @@ import android.os.StrictMode
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
import androidx.work.WorkManager
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
import androidx.work.WorkManager
import com.stevesoltys.seedvault.crypto.cryptoModule
import com.stevesoltys.seedvault.header.headerModule
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.metadataModule
import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.saf.storagePluginModuleSaf
import com.stevesoltys.seedvault.restore.RestoreViewModel
import com.stevesoltys.seedvault.restore.install.installModule
import com.stevesoltys.seedvault.settings.AppListRetriever
@ -54,15 +55,41 @@ open class App : Application() {
private val appModule = module {
single { SettingsManager(this@App) }
single { BackupNotificationManager(this@App) }
single { StoragePluginManager(this@App, get(), get()) }
single { Clock() }
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
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 { BackupStorageViewModel(this@App, get(), get(), get(), get()) }
viewModel { RestoreStorageViewModel(this@App, get(), get()) }
viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get()) }
viewModel { BackupStorageViewModel(this@App, get(), get(), get(), get(), get(), get()) }
viewModel { RestoreStorageViewModel(this@App, 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()) }
}
@ -100,7 +127,7 @@ open class App : Application() {
cryptoModule,
headerModule,
metadataModule,
documentsProviderModule, // storage plugin
storagePluginModuleSaf, // storage plugin
backupModule,
restoreModule,
installModule,

View file

@ -1,13 +1,11 @@
package com.stevesoltys.seedvault.plugins
import android.app.backup.RestoreSet
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.plugins.saf.SafStorage
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
interface StoragePlugin {
interface StoragePlugin<T> {
/**
* Returns true if the plugin is working, or false if it isn't.
@ -53,14 +51,6 @@ interface StoragePlugin {
@Throws(IOException::class)
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.
*

View file

@ -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)
}
}

View file

@ -10,12 +10,13 @@ import java.io.IOException
import java.io.InputStream
@WorkerThread
@Suppress("BlockingMethodInNonBlockingContext", "Deprecation") // all methods do I/O
@Suppress("Deprecation")
internal class DocumentsProviderLegacyPlugin(
private val context: Context,
private val storage: DocumentsStorage,
private val storageGetter: () -> DocumentsStorage,
) : LegacyStoragePlugin {
private val storage get() = storageGetter()
private var packageDir: DocumentFile? = null
private var packageChildren: List<DocumentFile>? = null

View file

@ -1,14 +1,22 @@
package com.stevesoltys.seedvault.plugins.saf
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.dsl.module
val documentsProviderModule = module {
single { DocumentsStorage(androidContext(), get()) }
val storagePluginModuleSaf = module {
single { SafFactory(androidContext(), get(), get()) }
single { SafHandler(androidContext(), get(), get(), get()) }
single<StoragePlugin> { DocumentsProviderStoragePlugin(androidContext(), get()) }
@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)
},
)
}
}

View file

@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.plugins.saf
import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.getStorageContext
@ -16,19 +17,15 @@ import java.io.OutputStream
private val TAG = DocumentsProviderStoragePlugin::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class DocumentsProviderStoragePlugin(
private val appContext: Context,
private val storage: DocumentsStorage,
) : StoragePlugin {
) : StoragePlugin<Uri> {
/**
* Attention: This context might be from a different user. Use with care.
*/
private val context: Context
get() = appContext.getStorageContext {
storage.safStorage?.isUsb == true
}
private val context: Context get() = appContext.getStorageContext { storage.safStorage.isUsb }
private val packageManager: PackageManager = appContext.packageManager
@ -77,16 +74,6 @@ internal class DocumentsProviderStoragePlugin(
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>? {
val rootDir = storage.rootBackupDir ?: return null
val backupSets = getBackups(context, rootDir)
@ -110,7 +97,6 @@ internal class DocumentsProviderStoragePlugin(
class BackupSet(val token: Long, val metadataFile: DocumentFile)
@Suppress("BlockingMethodInNonBlockingContext")
internal suspend fun getBackups(context: Context, rootDir: DocumentFile): List<BackupSet> {
val backupSets = ArrayList<BackupSet>()
val files = try {

View file

@ -1,5 +1,3 @@
@file:Suppress("BlockingMethodInNonBlockingContext")
package com.stevesoltys.seedvault.plugins.saf
import android.content.ContentResolver
@ -43,27 +41,19 @@ private val TAG = DocumentsStorage::class.java.simpleName
internal class DocumentsStorage(
private val appContext: Context,
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.
*/
private val context: Context
get() = appContext.getStorageContext {
safStorage?.isUsb == true
}
private val context: Context get() = appContext.getStorageContext { safStorage.isUsb }
private val contentResolver: ContentResolver get() = context.contentResolver
internal var rootBackupDir: DocumentFile? = null
get() = runBlocking {
if (field == null) {
val parent = safStorage?.getDocumentFile(context)
?: return@runBlocking null
val parent = safStorage.getDocumentFile(context)
field = try {
parent.createOrGetDirectory(context, DIRECTORY_ROOT).apply {
// 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.
*/
fun reset(newToken: Long?) {
safStorage = null
currentToken = newToken
rootBackupDir = null
currentSetDir = null
}
fun getAuthority(): String? = safStorage?.uri?.authority
fun getAuthority(): String? = safStorage.uri.authority
@Throws(IOException::class)
suspend fun getSetDir(token: Long = currentToken ?: error("no token")): DocumentFile? {

View file

@ -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)
}
}

View file

@ -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),
)
}
}

View file

@ -6,19 +6,21 @@
package com.stevesoltys.seedvault.plugins.saf
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.plugins.StorageProperties
data class SafStorage(
val uri: Uri,
val name: String,
val isUsb: Boolean,
val requiresNetwork: Boolean,
) {
fun getDocumentFile(context: Context) = DocumentFile.fromTreeUri(context, uri)
override val config: Uri,
override val name: String,
override val isUsb: Boolean,
override val requiresNetwork: Boolean,
) : StorageProperties<Uri>() {
val uri: Uri = config
fun getDocumentFile(context: Context) = DocumentFile.fromTreeUri(context, config)
?: throw AssertionError("Should only happen on API < 21.")
/**
@ -27,23 +29,7 @@ data class SafStorage(
* Must be run off UI thread (ideally I/O).
*/
@WorkerThread
fun isUnavailableUsb(context: Context): Boolean {
override 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)
}
}

View file

@ -1,17 +1,10 @@
package com.stevesoltys.seedvault.plugins.webdav
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
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.dsl.module
val webDavModule = module {
// TODO PluginManager should create the plugin on demand
single<StoragePlugin> { WebDavStoragePlugin(androidContext(), WebDavConfig("", "", "")) }
single { DocumentsStorage(androidContext(), get()) }
@Suppress("Deprecation")
single<LegacyStoragePlugin> { DocumentsProviderLegacyPlugin(androidContext(), get()) }
single<StoragePlugin<*>> { WebDavStoragePlugin(androidContext(), WebDavConfig("", "", "")) }
}

View file

@ -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_NO_MEDIA
import com.stevesoltys.seedvault.plugins.tokenRegex
import com.stevesoltys.seedvault.plugins.saf.SafStorage
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.io.IOException
import java.io.InputStream
@ -25,7 +24,7 @@ internal class WebDavStoragePlugin(
context: Context,
webDavConfig: WebDavConfig,
root: String = DIRECTORY_ROOT,
) : WebDavStorage(webDavConfig, root), StoragePlugin {
) : WebDavStorage(webDavConfig, root), StoragePlugin<WebDavConfig> {
override suspend fun test(): Boolean {
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>? {
return try {
doGetAvailableBackups()

View file

@ -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.UNKNOWN_ERROR
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_BACKUP
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_FILES
@ -81,8 +82,9 @@ internal class RestoreViewModel(
private val restoreCoordinator: RestoreCoordinator,
private val apkRestore: ApkRestore,
storageBackup: StorageBackup,
pluginManager: StoragePluginManager,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : RequireProvisioningViewModel(app, settingsManager, keyManager),
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager),
RestorableBackupClickListener, SnapshotViewModel {
override val isRestoreOperation = true

View file

@ -10,6 +10,7 @@ import com.stevesoltys.seedvault.metadata.ApkSplit
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.restore.RestorableBackup
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
@ -28,7 +29,7 @@ private val TAG = ApkRestore::class.java.simpleName
internal class ApkRestore(
private val context: Context,
private val storagePlugin: StoragePlugin,
private val pluginManager: StoragePluginManager,
@Suppress("Deprecation")
private val legacyStoragePlugin: LegacyStoragePlugin,
private val crypto: Crypto,
@ -37,6 +38,7 @@ internal class ApkRestore(
) {
private val pm = context.packageManager
private val storagePlugin get() = pluginManager.appPlugin
fun restore(backup: RestorableBackup) = flow {
// we don't filter out apps without APK, so the user can manually install them
@ -87,7 +89,7 @@ internal class ApkRestore(
emit(installResult)
}
@Suppress("ThrowsCount", "BlockingMethodInNonBlockingContext") // flows on Dispatcher.IO
@Suppress("ThrowsCount")
@Throws(IOException::class, SecurityException::class)
private suspend fun restore(
collector: FlowCollector<InstallResult>,
@ -212,7 +214,6 @@ internal class ApkRestore(
* @return a [Pair] of the cached [File] and SHA-256 hash.
*/
@Throws(IOException::class)
@Suppress("BlockingMethodInNonBlockingContext") // flows on Dispatcher.IO
private suspend fun cacheApk(
version: Byte,
token: Long,

View file

@ -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)
}
}
}

View file

@ -10,6 +10,7 @@ import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
@ -18,6 +19,7 @@ class SchedulingFragment : PreferenceFragmentCompat(),
private val viewModel: SettingsViewModel by sharedViewModel()
private val settingsManager: SettingsManager by inject()
private val storagePluginManager: StoragePluginManager by inject()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
permitDiskReads {
@ -29,7 +31,7 @@ class SchedulingFragment : PreferenceFragmentCompat(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val storage = settingsManager.getSafStorage()
val storage = storagePluginManager.storageProperties
if (storage?.isUsb == true) {
findPreference<PreferenceCategory>("scheduling_category_conditions")?.isEnabled = false
}

View file

@ -22,7 +22,8 @@ import androidx.preference.TwoStatePreference
import androidx.work.WorkInfo
import com.stevesoltys.seedvault.R
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.ui.toRelativeTime
import org.koin.android.ext.android.inject
@ -34,7 +35,7 @@ private val TAG = SettingsFragment::class.java.name
class SettingsFragment : PreferenceFragmentCompat() {
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 lateinit var backup: TwoStatePreference
@ -49,7 +50,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
private var menuBackupNow: 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?) {
permitDiskReads {
@ -165,7 +167,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
// we need to re-set the title when returning to this fragment
activity?.setTitle(R.string.backup)
safStorage = settingsManager.getSafStorage()
setBackupEnabledState()
setBackupLocationSummary()
setAutoRestoreState()
@ -242,7 +243,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
activity?.contentResolver?.let {
autoRestore.isChecked = Settings.Secure.getInt(it, BACKUP_AUTO_RESTORE, 1) == 1
}
val storage = this.safStorage
val storage = this.storageProperties
if (storage?.isUsb == true) {
autoRestore.summary = getString(R.string.settings_auto_restore_summary) + "\n\n" +
getString(R.string.settings_auto_restore_summary_usb, storage.name)
@ -253,7 +254,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
private fun setBackupLocationSummary() {
// 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?) {
@ -272,7 +274,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
* says that nothing is scheduled which can happen when backup destination is on flash drive.
*/
private fun setAppBackupSchedulingSummary(workInfo: WorkInfo?) {
if (safStorage?.isUsb == true) {
if (storageProperties?.isUsb == true) {
backupScheduling.summary = getString(R.string.settings_backup_status_next_backup_usb)
return
}

View file

@ -3,15 +3,14 @@ package com.stevesoltys.seedvault.settings
import android.content.Context
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.hardware.usb.UsbDevice
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import com.stevesoltys.seedvault.getStorageContext
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.transport.backup.BackupCoordinator
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_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_NAME = "storageName"
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
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) {
prefs.edit()
.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(
val name: String,
val serialNumber: String?,

View file

@ -34,6 +34,8 @@ import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.metadata.MetadataManager
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.StorageBackupService
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
@ -59,12 +61,13 @@ internal class SettingsViewModel(
app: Application,
settingsManager: SettingsManager,
keyManager: KeyManager,
private val pluginManager: StoragePluginManager,
private val metadataManager: MetadataManager,
private val appListRetriever: AppListRetriever,
private val storageBackup: StorageBackup,
private val backupManager: IBackupManager,
private val backupInitializer: BackupInitializer,
) : RequireProvisioningViewModel(app, settingsManager, keyManager) {
) : RequireProvisioningViewModel(app, settingsManager, keyManager, pluginManager) {
private val contentResolver = app.contentResolver
private val connectivityManager: ConnectivityManager? =
@ -131,9 +134,9 @@ internal class SettingsViewModel(
}
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) {
// disable storage backup if new storage is on USB
cancelAppBackup()
@ -149,25 +152,28 @@ internal class SettingsViewModel(
fun onWorkerStateChanged() {
viewModelScope.launch(Dispatchers.IO) {
val canDo = settingsManager.canDoBackupNow() &&
val canDo = pluginManager.canDoBackupNow() &&
appBackupWorkInfo.value?.state != WorkInfo.State.RUNNING
mBackupPossible.postValue(canDo)
}
}
private fun onStoragePropertiesChanged() {
val storage = settingsManager.getSafStorage() ?: return
val storage = pluginManager.storageProperties ?: return
Log.d(TAG, "onStoragePropertiesChanged")
if (storage is SafStorage) {
// register storage observer
try {
contentResolver.unregisterContentObserver(storageObserver)
contentResolver.registerContentObserver(storage.uri, false, storageObserver)
} catch (e: SecurityException) {
// This can happen if the app providing the storage was uninstalled.
// validLocationIsSet() gets called elsewhere and prompts for a new storage location.
// 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
if (networkCallback.registered && !storage.requiresNetwork) {
@ -301,7 +307,7 @@ internal class SettingsViewModel(
}
}
fun cancelAppBackup() {
private fun cancelAppBackup() {
AppBackupWorker.unschedule(app)
}

View file

@ -16,12 +16,8 @@ internal class SeedvaultSafStoragePlugin(
/**
* Attention: This context might be from a different user. Use with care.
*/
override val context: Context
get() = appContext.getStorageContext {
storage.safStorage?.isUsb == true
}
override val root: DocumentFile
get() = storage.rootBackupDir ?: error("No storage set")
override val context: Context get() = appContext.getStorageContext { storage.safStorage.isUsb }
override val root: DocumentFile get() = storage.rootBackupDir ?: error("No storage set")
override fun getMasterKey(): SecretKey = keyManager.getMainKey()
override fun hasMasterKey(): Boolean = keyManager.hasMainKey()

View file

@ -1,7 +1,7 @@
package com.stevesoltys.seedvault.storage
import android.content.Intent
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.worker.AppBackupWorker
import org.calyxos.backup.storage.api.BackupObserver
import org.calyxos.backup.storage.api.RestoreObserver
@ -34,7 +34,7 @@ internal class StorageBackupService : BackupService() {
}
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
override val backupObserver: BackupObserver by lazy {
@ -43,7 +43,7 @@ internal class StorageBackupService : BackupService() {
override fun onBackupFinished(intent: Intent, success: Boolean) {
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)
}
}

View file

@ -1,10 +1,9 @@
package com.stevesoltys.seedvault.storage
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import org.calyxos.backup.storage.api.StorageBackup
import org.calyxos.backup.storage.api.StoragePlugin
import org.koin.dsl.module
val storageModule = module {
single<StoragePlugin> { SeedvaultSafStoragePlugin(get(), get(), get()) }
single { StorageBackup(get(), get()) }
single { StorageBackup(get(), { get<StoragePluginManager>().filesPlugin }) }
}

View file

@ -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.UNKNOWN_ERROR
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.settings.SettingsManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
@ -54,11 +55,10 @@ private class CoordinatorState(
* @author Steve Soltys
* @author Torsten Grote
*/
@WorkerThread // entire class should always be accessed from a worker thread, so blocking is ok
@Suppress("BlockingMethodInNonBlockingContext")
@WorkerThread
internal class BackupCoordinator(
private val context: Context,
private val plugin: StoragePlugin,
private val pluginManager: StoragePluginManager,
private val kv: KVBackup,
private val full: FullBackup,
private val clock: Clock,
@ -68,6 +68,7 @@ internal class BackupCoordinator(
private val nm: BackupNotificationManager,
) {
private val plugin get() = pluginManager.appPlugin
private val state = CoordinatorState(
calledInitialize = false,
calledClearBackupData = false,
@ -126,7 +127,7 @@ internal class BackupCoordinator(
} catch (e: Exception) {
Log.e(TAG, "Error initializing device", e)
// 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
}
@ -354,7 +355,7 @@ internal class BackupCoordinator(
if (result == TRANSPORT_OK) {
val isNormalBackup = packageName != MAGIC_PACKAGE_MANAGER
// call onPackageBackedUp for @pm@ only if we can do backups right now
if (isNormalBackup || settingsManager.canDoBackupNow()) {
if (isNormalBackup || pluginManager.canDoBackupNow()) {
try {
onPackageBackedUp(packageInfo, BackupType.KV, size)
} catch (e: Exception) {
@ -411,7 +412,7 @@ internal class BackupCoordinator(
val longBackoff = DAYS.toMillis(30)
// back off if there's no storage set
val storage = settingsManager.getSafStorage() ?: return longBackoff
val storage = pluginManager.storageProperties ?: return longBackoff
return when {
// back off if storage is removable and not available right now
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")
return getOutputStream(t, FILE_BACKUP_METADATA)
}

View file

@ -11,38 +11,38 @@ val backupModule = module {
context = androidContext(),
backupManager = get(),
settingsManager = get(),
plugin = get()
pluginManager = get(),
)
}
single<KvDbManager> { KvDbManagerImpl(androidContext()) }
single {
KVBackup(
plugin = get(),
pluginManager = get(),
settingsManager = get(),
inputFactory = get(),
crypto = get(),
dbManager = get()
dbManager = get(),
)
}
single {
FullBackup(
plugin = get(),
pluginManager = get(),
settingsManager = get(),
inputFactory = get(),
crypto = get()
crypto = get(),
)
}
single {
BackupCoordinator(
context = androidContext(),
plugin = get(),
pluginManager = get(),
kv = get(),
full = get(),
clock = get(),
packageService = get(),
metadataManager = get(),
settingsManager = get(),
nm = get()
nm = get(),
)
}
}

View file

@ -11,7 +11,7 @@ import android.util.Log
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.header.VERSION
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 libcore.io.IoUtils.closeQuietly
import java.io.EOFException
@ -39,12 +39,13 @@ private val TAG = FullBackup::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class FullBackup(
private val plugin: StoragePlugin,
private val pluginManager: StoragePluginManager,
private val settingsManager: SettingsManager,
private val inputFactory: InputFactory,
private val crypto: Crypto,
) {
private val plugin get() = pluginManager.appPlugin
private var state: FullBackupState? = null
fun hasState() = state != null

View file

@ -13,7 +13,7 @@ import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.header.VERSION
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 java.io.IOException
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
@Suppress("BlockingMethodInNonBlockingContext")
internal class KVBackup(
private val plugin: StoragePlugin,
private val pluginManager: StoragePluginManager,
private val settingsManager: SettingsManager,
private val inputFactory: InputFactory,
private val crypto: Crypto,
private val dbManager: KvDbManager,
) {
private val plugin get() = pluginManager.appPlugin
private var state: KVBackupState? = null
fun hasState() = state != null
@ -138,7 +138,7 @@ internal class KVBackup(
// K/V backups (typically starting with package manager metadata - @pm@)
// are scheduled with JobInfo.Builder#setOverrideDeadline()
// and thus do not respect backoff.
settingsManager.canDoBackupNow()
pluginManager.canDoBackupNow()
} else {
// all other packages always need upload
true

View file

@ -18,6 +18,7 @@ import android.util.Log.INFO
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.settings.SettingsManager
private val TAG = PackageService::class.java.simpleName
@ -32,11 +33,12 @@ internal class PackageService(
private val context: Context,
private val backupManager: IBackupManager,
private val settingsManager: SettingsManager,
private val plugin: StoragePlugin,
private val pluginManager: StoragePluginManager,
) {
private val packageManager: PackageManager = context.packageManager
private val myUserId = UserHandle.myUserId()
private val plugin: StoragePlugin<*> get() = pluginManager.appPlugin
val eligiblePackages: List<String>
@WorkerThread

View file

@ -13,7 +13,7 @@ import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH
import com.stevesoltys.seedvault.header.UnsupportedVersionException
import com.stevesoltys.seedvault.header.getADForFull
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import libcore.io.IoUtils.closeQuietly
import java.io.EOFException
import java.io.IOException
@ -32,9 +32,8 @@ private class FullRestoreState(
private val TAG = FullRestore::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class FullRestore(
private val plugin: StoragePlugin,
private val pluginManager: StoragePluginManager,
@Suppress("Deprecation")
private val legacyPlugin: LegacyStoragePlugin,
private val outputFactory: OutputFactory,
@ -42,6 +41,7 @@ internal class FullRestore(
private val crypto: Crypto,
) {
private val plugin get() = pluginManager.appPlugin
private var state: FullRestoreState? = null
fun hasState() = state != null

View file

@ -16,13 +16,12 @@ import com.stevesoltys.seedvault.header.UnsupportedVersionException
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForKV
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.KvDbManager
import libcore.io.IoUtils.closeQuietly
import java.io.IOException
import java.security.GeneralSecurityException
import java.util.ArrayList
import java.util.zip.GZIPInputStream
import javax.crypto.AEADBadTagException
@ -39,9 +38,8 @@ private class KVRestoreState(
private val TAG = KVRestore::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class KVRestore(
private val plugin: StoragePlugin,
private val pluginManager: StoragePluginManager,
@Suppress("Deprecation")
private val legacyPlugin: LegacyStoragePlugin,
private val outputFactory: OutputFactory,
@ -50,6 +48,7 @@ internal class KVRestore(
private val dbManager: KvDbManager,
) {
private val plugin get() = pluginManager.appPlugin
private var state: KVRestoreState? = null
/**

View file

@ -21,6 +21,7 @@ import com.stevesoltys.seedvault.metadata.DecryptionFailedException
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.MetadataReader
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.D2D_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
@Suppress("BlockingMethodInNonBlockingContext")
internal class RestoreCoordinator(
private val context: Context,
private val crypto: Crypto,
private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager,
private val notificationManager: BackupNotificationManager,
private val plugin: StoragePlugin,
private val pluginManager: StoragePluginManager,
private val kv: KVRestore,
private val full: FullRestore,
private val metadataReader: MetadataReader,
) {
private val plugin: StoragePlugin<*> get() = pluginManager.appPlugin
private var state: RestoreCoordinatorState? = null
private var backupMetadata: BackupMetadata? = null
private val failedPackages = ArrayList<String>()
@ -169,7 +170,7 @@ internal class RestoreCoordinator(
// check if we even have a backup of that app
if (metadataManager.getPackageMetadata(pmPackageName) != null) {
// 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)
notificationManager.onRemovableStorageNotAvailableForRestore(
pmPackageName,
@ -363,9 +364,8 @@ internal class RestoreCoordinator(
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 {
val storage = settingsManager.getSafStorage() ?: return false
val storage = pluginManager.storageProperties ?: return false
return storage.isUnavailableUsb(context)
}

View file

@ -3,13 +3,14 @@ package com.stevesoltys.seedvault.ui
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.storage.StorageViewModel
abstract class RequireProvisioningViewModel(
protected val app: Application,
protected val settingsManager: SettingsManager,
protected val keyManager: KeyManager,
private val pluginManager: StoragePluginManager,
) : AndroidViewModel(app) {
abstract val isRestoreOperation: Boolean
@ -18,7 +19,7 @@ abstract class RequireProvisioningViewModel(
internal val chooseBackupLocation: LiveEvent<Boolean> get() = mChooseBackupLocation
internal fun chooseBackupLocation() = mChooseBackupLocation.setEvent(true)
internal fun validLocationIsSet() = StorageViewModel.validLocationIsSet(app, settingsManager)
internal fun validLocationIsSet() = pluginManager.isValidAppPluginSet()
internal fun recoveryCodeIsSet() = keyManager.hasBackupKey()

View file

@ -3,11 +3,13 @@ package com.stevesoltys.seedvault.ui.storage
import android.app.Application
import android.app.backup.IBackupManager
import android.app.job.JobInfo
import android.net.Uri
import android.util.Log
import androidx.lifecycle.viewModelScope
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
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.storage.StorageBackupJobService
import com.stevesoltys.seedvault.transport.backup.BackupInitializer
@ -26,14 +28,17 @@ internal class BackupStorageViewModel(
private val backupManager: IBackupManager,
private val backupInitializer: BackupInitializer,
private val storageBackup: StorageBackup,
safHandler: SafHandler,
settingsManager: SettingsManager,
) : StorageViewModel(app, settingsManager) {
storagePluginManager: StoragePluginManager,
) : StorageViewModel(app, safHandler, settingsManager, storagePluginManager) {
override val isRestoreOperation = false
override fun onSafUriSet(uri: Uri) {
val isUsb = saveStorage(uri)
if (isUsb) {
override fun onSafUriSet(safStorage: SafStorage) {
safHandler.save(safStorage)
safHandler.setPlugin(safStorage)
if (safStorage.isUsb) {
// disable storage backup if new storage is on USB
cancelBackupWorkers()
} else {
@ -41,12 +46,16 @@ internal class BackupStorageViewModel(
// also to update the network requirement of the new storage
scheduleBackupWorkers()
}
onStorageLocationSet(safStorage.isUsb)
}
private fun onStorageLocationSet(isUsb: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
try {
// remove old storage snapshots and clear cache
// TODO is this needed? It also does create all 255 chunk folders which takes time
// 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()
try {
// initialize the new location (if backups are enabled)
if (backupManager.isBackupEnabled) {
val onError = {
@ -74,7 +83,7 @@ internal class BackupStorageViewModel(
}
private fun scheduleBackupWorkers() {
val storage = settingsManager.getSafStorage() ?: error("no storage available")
val storage = storagePluginManager.storageProperties ?: error("no storage available")
if (!storage.isUsb) {
if (backupManager.isBackupEnabled) {
AppBackupWorker.schedule(app, settingsManager, CANCEL_AND_REENQUEUE)

View file

@ -1,12 +1,13 @@
package com.stevesoltys.seedvault.ui.storage
import android.app.Application
import android.net.Uri
import android.util.Log
import androidx.lifecycle.viewModelScope
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -16,26 +17,27 @@ private val TAG = RestoreStorageViewModel::class.java.simpleName
internal class RestoreStorageViewModel(
private val app: Application,
private val storagePlugin: StoragePlugin,
safHandler: SafHandler,
settingsManager: SettingsManager,
) : StorageViewModel(app, settingsManager) {
storagePluginManager: StoragePluginManager,
) : StorageViewModel(app, safHandler, settingsManager, storagePluginManager) {
override val isRestoreOperation = true
override fun onSafUriSet(uri: Uri) {
override fun onSafUriSet(safStorage: SafStorage) {
viewModelScope.launch(Dispatchers.IO) {
val storage = createStorage(uri)
val hasBackup = try {
storagePlugin.hasBackup(storage)
safHandler.hasAppBackup(safStorage)
} catch (e: IOException) {
Log.e(TAG, "Error reading URI: $uri", e)
Log.e(TAG, "Error reading URI: ${safStorage.uri}", e)
false
}
if (hasBackup) {
saveStorage(storage)
safHandler.save(safStorage)
safHandler.setPlugin(safStorage)
mLocationChecked.postEvent(LocationResult())
} else {
Log.w(TAG, "Location was rejected: $uri")
Log.w(TAG, "Location was rejected: ${safStorage.uri}")
// notify the UI that the location was invalid
val errorMsg =
@ -44,5 +46,4 @@ internal class RestoreStorageViewModel(
}
}
}
}

View file

@ -62,9 +62,7 @@ internal class StorageOptionFetcher(private val context: Context, private val is
internal fun getRemovableStorageListener() = listener
internal fun getStorageOptions(): List<StorageOption> {
val roots = ArrayList<StorageOption>().apply {
add(WebDavOption(context))
}
val roots = ArrayList<StorageOption>()
val intent = Intent(PROVIDER_INTERFACE)
val providers = packageManager.queryIntentContentProviders(intent, 0)
for (info in providers) {

View file

@ -2,23 +2,15 @@ package com.stevesoltys.seedvault.ui.storage
import android.annotation.UiThread
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.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.isMassStorage
import com.stevesoltys.seedvault.permitDiskReads
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.BackupManagerSettings
import com.stevesoltys.seedvault.settings.FlashDrive
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.LiveEvent
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.launch
private val TAG = StorageViewModel::class.java.simpleName
internal abstract class StorageViewModel(
private val app: Application,
protected val safHandler: SafHandler,
protected val settingsManager: SettingsManager,
protected val storagePluginManager: StoragePluginManager,
) : AndroidViewModel(app), RemovableStorageListener {
private val mStorageOptions = MutableLiveData<List<StorageOption>>()
@ -47,22 +39,9 @@ internal abstract class StorageViewModel(
internal var isSetupWizard: Boolean = false
internal val hasStorageSet: Boolean
get() = settingsManager.getSafStorage() != null
get() = storagePluginManager.storageProperties != null
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() {
if (storageOptionFetcher.getRemovableStorageListener() == null) {
storageOptionFetcher.setRemovableStorageListener(this)
@ -74,6 +53,10 @@ internal abstract class StorageViewModel(
override fun onStorageChanged() = loadStorageRoots()
/**
* Remembers that the user chose SAF.
* Usually followed by a call of [onUriPermissionResultReceived].
*/
fun onSafOptionChosen(option: SafOption) {
safOption = option
}
@ -84,71 +67,18 @@ internal abstract class StorageViewModel(
mLocationChecked.setEvent(LocationResult(msg))
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
mLocationSet.setEvent(true)
// persist permission to access backup folder across reboots
val takeFlags = FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
app.contentResolver.takePersistableUriPermission(uri, takeFlags)
onSafUriSet(uri)
onSafUriSet(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)
abstract fun onSafUriSet(safStorage: SafStorage)
override fun onCleared() {
storageOptionFetcher.setRemovableStorageListener(null)

View file

@ -30,7 +30,6 @@ import java.security.MessageDigest
private val TAG = ApkBackup::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class ApkBackup(
private val pm: PackageManager,
private val crypto: Crypto,

View file

@ -12,6 +12,7 @@ import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
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.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.PackageService
@ -28,7 +29,7 @@ internal class ApkBackupManager(
private val metadataManager: MetadataManager,
private val packageService: PackageService,
private val apkBackup: ApkBackup,
private val plugin: StoragePlugin,
private val pluginManager: StoragePluginManager,
private val nm: BackupNotificationManager,
) {
@ -50,7 +51,7 @@ internal class ApkBackupManager(
keepTrying {
// upload all local changes only at the end,
// so we don't have to re-upload the metadata
plugin.getMetadataOutputStream().use { outputStream ->
pluginManager.appPlugin.getMetadataOutputStream().use { outputStream ->
metadataManager.uploadMetadata(outputStream)
}
}
@ -102,7 +103,7 @@ internal class ApkBackupManager(
return try {
apkBackup.backupApkIfNecessary(packageInfo) { name ->
val token = settingsManager.getToken() ?: throw IOException("no current token")
plugin.getOutputStream(token, name)
pluginManager.appPlugin.getOutputStream(token, name)
}?.let { packageMetadata ->
metadataManager.onApkBackedUp(packageInfo, packageMetadata)
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")
return getOutputStream(t, FILE_BACKUP_METADATA)
}

View file

@ -31,7 +31,7 @@ val workerModule = module {
metadataManager = get(),
packageService = get(),
apkBackup = get(),
plugin = get(),
pluginManager = get(),
nm = get()
)
}

View file

@ -8,7 +8,7 @@ import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
import com.stevesoltys.seedvault.header.headerModule
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.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.backupModule
@ -42,7 +42,7 @@ class TestApp : App() {
testCryptoModule,
headerModule,
metadataModule,
documentsProviderModule, // storage plugin
storagePluginModuleSaf, // storage plugin
backupModule,
restoreModule,
installModule,

View file

@ -11,7 +11,6 @@ import io.mockk.mockkStatic
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
@Suppress("BlockingMethodInNonBlockingContext")
internal class StoragePluginTest : BackupTest() {
private val storage = mockk<DocumentsStorage>()
@ -39,7 +38,7 @@ internal class StoragePluginTest : BackupTest() {
// get current set dir and for that the current token
every { storage getProperty "currentToken" } 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
// delete contents of current set dir
coEvery { setDir.listFilesBlocking(context) } returns listOf(backupFile)

View file

@ -12,6 +12,7 @@ import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.restore.RestorableBackup
import com.stevesoltys.seedvault.transport.TransportTest
import com.stevesoltys.seedvault.worker.ApkBackup
@ -38,7 +39,6 @@ import java.nio.file.Path
import kotlin.random.Random
@ExperimentalCoroutinesApi
@Suppress("BlockingMethodInNonBlockingContext")
internal class ApkBackupRestoreTest : TransportTest() {
private val pm: PackageManager = mockk()
@ -46,16 +46,17 @@ internal class ApkBackupRestoreTest : TransportTest() {
every { packageManager } returns pm
}
private val storagePluginManager: StoragePluginManager = mockk()
@Suppress("Deprecation")
private val legacyStoragePlugin: LegacyStoragePlugin = mockk()
private val storagePlugin: StoragePlugin = mockk()
private val storagePlugin: StoragePlugin<*> = mockk()
private val splitCompatChecker: ApkSplitCompatibilityChecker = mockk()
private val apkInstaller: ApkInstaller = mockk()
private val apkBackup = ApkBackup(pm, crypto, settingsManager, metadataManager)
private val apkRestore: ApkRestore = ApkRestore(
context = strictContext,
storagePlugin = storagePlugin,
pluginManager = storagePluginManager,
legacyStoragePlugin = legacyStoragePlugin,
crypto = crypto,
splitCompatChecker = splitCompatChecker,
@ -90,6 +91,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
init {
mockkStatic(PackageUtils::class)
every { storagePluginManager.appPlugin } returns storagePlugin
}
@Test

View file

@ -15,6 +15,7 @@ import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.restore.RestorableBackup
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
@ -41,7 +42,6 @@ import java.io.IOException
import java.nio.file.Path
import kotlin.random.Random
@Suppress("BlockingMethodInNonBlockingContext")
@ExperimentalCoroutinesApi
internal class ApkRestoreTest : TransportTest() {
@ -49,18 +49,19 @@ internal class ApkRestoreTest : TransportTest() {
private val strictContext: Context = mockk<Context>().apply {
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 splitCompatChecker: ApkSplitCompatibilityChecker = mockk()
private val apkInstaller: ApkInstaller = mockk()
private val apkRestore: ApkRestore = ApkRestore(
strictContext,
storagePlugin,
legacyStoragePlugin,
crypto,
splitCompatChecker,
apkInstaller
context = strictContext,
pluginManager = storagePluginManager,
legacyStoragePlugin = legacyStoragePlugin,
crypto = crypto,
splitCompatChecker = splitCompatChecker,
apkInstaller = apkInstaller,
)
private val icon: Drawable = mockk()
@ -85,6 +86,8 @@ internal class ApkRestoreTest : TransportTest() {
init {
// as we don't do strict signature checking, we can use a relaxed mock
packageInfo.signingInfo = mockk(relaxed = true)
every { storagePluginManager.appPlugin } returns storagePlugin
}
@Test

View file

@ -17,6 +17,7 @@ import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
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.transport.backup.BackupCoordinator
import com.stevesoltys.seedvault.transport.backup.FullBackup
@ -46,7 +47,6 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import kotlin.random.Random
@Suppress("BlockingMethodInNonBlockingContext")
internal class CoordinatorIntegrationTest : TransportTest() {
private val inputFactory = mockk<InputFactory>()
@ -58,18 +58,20 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val metadataReader = MetadataReaderImpl(cryptoImpl)
private val notificationManager = mockk<BackupNotificationManager>()
private val dbManager = TestKvDbManager()
private val storagePluginManager: StoragePluginManager = mockk()
@Suppress("Deprecation")
private val legacyPlugin = mockk<LegacyStoragePlugin>()
private val backupPlugin = mockk<StoragePlugin>()
private val backupPlugin = mockk<StoragePlugin<*>>()
private val kvBackup =
KVBackup(backupPlugin, settingsManager, inputFactory, cryptoImpl, dbManager)
private val fullBackup = FullBackup(backupPlugin, settingsManager, inputFactory, cryptoImpl)
KVBackup(storagePluginManager, settingsManager, inputFactory, cryptoImpl, dbManager)
private val fullBackup =
FullBackup(storagePluginManager, settingsManager, inputFactory, cryptoImpl)
private val apkBackup = mockk<ApkBackup>()
private val packageService: PackageService = mockk()
private val backup = BackupCoordinator(
context,
backupPlugin,
storagePluginManager,
kvBackup,
fullBackup,
clock,
@ -80,7 +82,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
)
private val kvRestore = KVRestore(
backupPlugin,
storagePluginManager,
legacyPlugin,
outputFactory,
headerReader,
@ -88,14 +90,14 @@ internal class CoordinatorIntegrationTest : TransportTest() {
dbManager
)
private val fullRestore =
FullRestore(backupPlugin, legacyPlugin, outputFactory, headerReader, cryptoImpl)
FullRestore(storagePluginManager, legacyPlugin, outputFactory, headerReader, cryptoImpl)
private val restore = RestoreCoordinator(
context,
crypto,
settingsManager,
metadataManager,
notificationManager,
backupPlugin,
storagePluginManager,
kvRestore,
fullRestore,
metadataReader
@ -113,6 +115,10 @@ internal class CoordinatorIntegrationTest : TransportTest() {
// as we use real crypto, we need a real name for packageInfo
private val realName = cryptoImpl.getNameForPackage(salt, packageInfo.packageName)
init {
every { storagePluginManager.appPlugin } returns backupPlugin
}
@Test
fun `test key-value backup and restore with 2 records`() = runBlocking {
val value = CapturingSlot<ByteArray>()

View file

@ -16,6 +16,7 @@ import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
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.SafStorage
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
@ -33,10 +34,9 @@ import java.io.IOException
import java.io.OutputStream
import kotlin.random.Random
@Suppress("BlockingMethodInNonBlockingContext")
internal class BackupCoordinatorTest : BackupTest() {
private val plugin = mockk<StoragePlugin>()
private val pluginManager = mockk<StoragePluginManager>()
private val kv = mockk<KVBackup>()
private val full = mockk<FullBackup>()
private val apkBackup = mockk<ApkBackup>()
@ -44,27 +44,32 @@ internal class BackupCoordinatorTest : BackupTest() {
private val packageService = mockk<PackageService>()
private val backup = BackupCoordinator(
context,
plugin,
kv,
full,
clock,
packageService,
metadataManager,
settingsManager,
notificationManager
context = context,
pluginManager = pluginManager,
kv = kv,
full = full,
clock = clock,
packageService = packageService,
metadataManager = metadataManager,
settingsManager = settingsManager,
nm = notificationManager,
)
private val plugin = mockk<StoragePlugin<*>>()
private val metadataOutputStream = mockk<OutputStream>()
private val fileDescriptor: ParcelFileDescriptor = mockk()
private val packageMetadata: PackageMetadata = mockk()
private val safStorage = SafStorage(
uri = Uri.EMPTY,
config = Uri.EMPTY,
name = getRandomString(),
isUsb = false,
requiresNetwork = false
requiresNetwork = false,
)
init {
every { pluginManager.appPlugin } returns plugin
}
@Test
fun `device initialization succeeds and delegates to plugin`() = runBlocking {
expectStartNewRestoreSet()
@ -90,7 +95,7 @@ internal class BackupCoordinatorTest : BackupTest() {
expectStartNewRestoreSet()
coEvery { plugin.initializeDevice() } throws IOException()
every { metadataManager.requiresInit } returns maybeTrue
every { settingsManager.canDoBackupNow() } returns !maybeTrue
every { pluginManager.canDoBackupNow() } returns !maybeTrue
every { notificationManager.onBackupError() } just Runs
assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
@ -109,7 +114,7 @@ internal class BackupCoordinatorTest : BackupTest() {
expectStartNewRestoreSet()
coEvery { plugin.initializeDevice() } throws IOException()
every { metadataManager.requiresInit } returns false
every { settingsManager.canDoBackupNow() } returns false
every { pluginManager.canDoBackupNow() } returns false
assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
@ -125,7 +130,7 @@ internal class BackupCoordinatorTest : BackupTest() {
fun `performIncrementalBackup of @pm@ causes re-init when legacy format`() = runBlocking {
val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
every { settingsManager.canDoBackupNow() } returns true
every { pluginManager.canDoBackupNow() } returns true
every { metadataManager.requiresInit } returns true
// start new restore set
@ -224,7 +229,7 @@ internal class BackupCoordinatorTest : BackupTest() {
every { kv.getCurrentSize() } returns 42L
coEvery { kv.finishBackup() } returns TRANSPORT_OK
every { settingsManager.canDoBackupNow() } returns false
every { pluginManager.canDoBackupNow() } returns false
assertEquals(TRANSPORT_OK, backup.finishBackup())
}
@ -290,7 +295,7 @@ internal class BackupCoordinatorTest : BackupTest() {
)
} 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 { metadataOutputStream.close() } just Runs
@ -340,7 +345,7 @@ internal class BackupCoordinatorTest : BackupTest() {
)
} 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 { metadataOutputStream.close() } just Runs

View file

@ -7,6 +7,7 @@ import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForFull
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.every
@ -21,16 +22,20 @@ import java.io.FileInputStream
import java.io.IOException
import kotlin.random.Random
@Suppress("BlockingMethodInNonBlockingContext")
internal class FullBackupTest : BackupTest() {
private val plugin = mockk<StoragePlugin>()
private val backup = FullBackup(plugin, settingsManager, inputFactory, crypto)
private val storagePluginManager: StoragePluginManager = mockk()
private val plugin = mockk<StoragePlugin<*>>()
private val backup = FullBackup(storagePluginManager, settingsManager, inputFactory, crypto)
private val bytes = ByteArray(23).apply { Random.nextBytes(this) }
private val inputStream = mockk<FileInputStream>()
private val ad = getADForFull(VERSION, packageInfo.packageName)
init {
every { storagePluginManager.appPlugin } returns plugin
}
@Test
fun `has no initial state`() {
assertFalse(backup.hasState())

View file

@ -13,6 +13,7 @@ import com.stevesoltys.seedvault.header.MAX_KEY_LENGTH_SIZE
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForKV
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import io.mockk.CapturingSlot
import io.mockk.Runs
import io.mockk.coEvery
@ -30,22 +31,26 @@ import java.io.ByteArrayInputStream
import java.io.IOException
import kotlin.random.Random
@Suppress("BlockingMethodInNonBlockingContext")
internal class KVBackupTest : BackupTest() {
private val plugin = mockk<StoragePlugin>()
private val pluginManager = mockk<StoragePluginManager>()
private val dataInput = mockk<BackupDataInput>()
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 plugin = mockk<StoragePlugin<*>>()
private val packageName = packageInfo.packageName
private val key = getRandomString(MAX_KEY_LENGTH_SIZE)
private val dataValue = Random.nextBytes(23)
private val dbBytes = Random.nextBytes(42)
private val inputStream = ByteArrayInputStream(dbBytes)
init {
every { pluginManager.appPlugin } returns plugin
}
@Test
fun `has no initial state`() {
assertFalse(backup.hasState())
@ -231,7 +236,7 @@ internal class KVBackupTest : BackupTest() {
every { dbManager.existsDb(pmPackageInfo.packageName) } returns false
every { crypto.getNameForPackage(salt, pmPackageInfo.packageName) } returns name
every { dbManager.getDb(pmPackageInfo.packageName) } returns db
every { settingsManager.canDoBackupNow() } returns false
every { pluginManager.canDoBackupNow() } returns false
every { db.put(key, dataValue) } just Runs
getDataInput(listOf(true, false))

View file

@ -13,6 +13,7 @@ import com.stevesoltys.seedvault.header.VersionHeader
import com.stevesoltys.seedvault.header.getADForFull
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import io.mockk.CapturingSlot
import io.mockk.Runs
import io.mockk.coEvery
@ -31,17 +32,27 @@ import java.io.IOException
import java.security.GeneralSecurityException
import kotlin.random.Random
@Suppress("BlockingMethodInNonBlockingContext")
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 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 outputStream = ByteArrayOutputStream()
private val ad = getADForFull(VERSION, packageInfo.packageName)
init {
every { storagePluginManager.appPlugin } returns plugin
}
@Test
fun `has no initial state`() {
assertFalse(restore.hasState())

View file

@ -12,6 +12,7 @@ import com.stevesoltys.seedvault.header.VersionHeader
import com.stevesoltys.seedvault.header.getADForKV
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.KvDbManager
import io.mockk.Runs
@ -33,15 +34,22 @@ import java.security.GeneralSecurityException
import java.util.zip.GZIPOutputStream
import kotlin.random.Random
@Suppress("BlockingMethodInNonBlockingContext")
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 dbManager = mockk<KvDbManager>()
private val output = mockk<BackupDataOutput>()
private val restore =
KVRestore(plugin, legacyPlugin, outputFactory, headerReader, crypto, dbManager)
private val restore = KVRestore(
pluginManager = storagePluginManager,
legacyPlugin = legacyPlugin,
outputFactory = outputFactory,
headerReader = headerReader,
crypto = crypto,
dbManager = dbManager,
)
private val db = mockk<KVDb>()
private val ad = getADForKV(VERSION, packageInfo.packageName)
@ -60,6 +68,8 @@ internal class KVRestoreTest : RestoreTest() {
init {
// for InputStream#readBytes()
mockkStatic("kotlin.io.ByteStreamsKt")
every { storagePluginManager.appPlugin } returns plugin
}
@Test
@ -180,7 +190,6 @@ internal class KVRestoreTest : RestoreTest() {
}
@Test
@Suppress("Deprecation")
fun `v0 listing records throws`() = runBlocking {
restore.initializeState(0x00, token, name, packageInfo)

View file

@ -16,6 +16,7 @@ import com.stevesoltys.seedvault.metadata.MetadataReader
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.plugins.EncryptedMetadata
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.saf.SafStorage
import com.stevesoltys.seedvault.transport.TransportTest
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
@ -35,25 +36,25 @@ import java.io.IOException
import java.io.InputStream
import kotlin.random.Random
@Suppress("BlockingMethodInNonBlockingContext")
internal class RestoreCoordinatorTest : TransportTest() {
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 full = mockk<FullRestore>()
private val metadataReader = mockk<MetadataReader>()
private val restore = RestoreCoordinator(
context,
crypto,
settingsManager,
metadataManager,
notificationManager,
plugin,
kv,
full,
metadataReader
context = context,
crypto = crypto,
settingsManager = settingsManager,
metadataManager = metadataManager,
notificationManager = notificationManager,
pluginManager = storagePluginManager,
kv = kv,
full = full,
metadataReader = metadataReader,
)
private val inputStream = mockk<InputStream>()
@ -71,6 +72,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
init {
metadata.packageMetadataMap[packageInfo2.packageName] =
PackageMetadata(backupType = BackupType.FULL)
every { storagePluginManager.appPlugin } returns plugin
}
@Test
@ -164,7 +167,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
fun `startRestore() optimized auto-restore with removed storage shows notification`() =
runBlocking {
every { settingsManager.getSafStorage() } returns safStorage
every { storagePluginManager.storageProperties } returns safStorage
every { safStorage.isUnavailableUsb(context) } returns true
every { metadataManager.getPackageMetadata(packageName) } returns PackageMetadata(42L)
every { safStorage.name } returns storageName
@ -188,7 +191,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
fun `startRestore() optimized auto-restore with available storage shows no notification`() =
runBlocking {
every { settingsManager.getSafStorage() } returns safStorage
every { storagePluginManager.storageProperties } returns safStorage
every { safStorage.isUnavailableUsb(context) } returns false
restore.beforeStartRestore(metadata)
@ -204,7 +207,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test
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 { metadataManager.getPackageMetadata(packageName) } returns null

View file

@ -15,6 +15,7 @@ import com.stevesoltys.seedvault.header.HeaderReaderImpl
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
import com.stevesoltys.seedvault.plugins.LegacyStoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePlugin
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.toByteArrayFromHex
import com.stevesoltys.seedvault.transport.TransportTest
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.
*/
@Suppress("BlockingMethodInNonBlockingContext")
internal class RestoreV0IntegrationTest : TransportTest() {
private val outputFactory = mockk<OutputFactory>()
@ -49,30 +49,31 @@ internal class RestoreV0IntegrationTest : TransportTest() {
private val dbManager = mockk<KvDbManager>()
private val metadataReader = MetadataReaderImpl(cryptoImpl)
private val notificationManager = mockk<BackupNotificationManager>()
private val storagePluginManager: StoragePluginManager = mockk()
@Suppress("Deprecation")
private val legacyPlugin = mockk<LegacyStoragePlugin>()
private val backupPlugin = mockk<StoragePlugin>()
private val backupPlugin = mockk<StoragePlugin<*>>()
private val kvRestore = KVRestore(
backupPlugin,
legacyPlugin,
outputFactory,
headerReader,
cryptoImpl,
dbManager
pluginManager = storagePluginManager,
legacyPlugin = legacyPlugin,
outputFactory = outputFactory,
headerReader = headerReader,
crypto = cryptoImpl,
dbManager = dbManager,
)
private val fullRestore =
FullRestore(backupPlugin, legacyPlugin, outputFactory, headerReader, cryptoImpl)
FullRestore(storagePluginManager, legacyPlugin, outputFactory, headerReader, cryptoImpl)
private val restore = RestoreCoordinator(
context,
crypto,
settingsManager,
metadataManager,
notificationManager,
backupPlugin,
kvRestore,
fullRestore,
metadataReader
context = context,
crypto = crypto,
settingsManager = settingsManager,
metadataManager = metadataManager,
notificationManager = notificationManager,
pluginManager = storagePluginManager,
kv = kvRestore,
full = fullRestore,
metadataReader = metadataReader,
).apply { beforeStartRestore(metadata.copy(version = 0x00)) }
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
@ -116,6 +117,10 @@ internal class RestoreV0IntegrationTest : TransportTest() {
private val key2 = "RestoreKey2"
private val key264 = key2.encodeBase64()
init {
every { storagePluginManager.appPlugin } returns backupPlugin
}
@Test
fun `test key-value backup and restore with 2 records`() = runBlocking {
val encryptedAppData = ("00002A2C701AA7C91D1286E265D29169B25C41E6D0" +

View file

@ -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.WAS_STOPPED
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.transport.TransportTest
import com.stevesoltys.seedvault.transport.backup.PackageService
@ -32,7 +33,8 @@ internal class ApkBackupManagerTest : TransportTest() {
private val packageService: PackageService = 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 apkBackupManager = ApkBackupManager(
@ -41,13 +43,17 @@ internal class ApkBackupManagerTest : TransportTest() {
metadataManager = metadataManager,
packageService = packageService,
apkBackup = apkBackup,
plugin = plugin,
pluginManager = storagePluginManager,
nm = nm,
)
private val metadataOutputStream = mockk<OutputStream>()
private val packageMetadata: PackageMetadata = mockk()
init {
every { storagePluginManager.appPlugin } returns plugin
}
@Test
fun `Package state of app that is not stopped gets recorded as not-allowed`() = runBlocking {
every { nm.onAppsNotBackedUp() } just Runs

View file

@ -38,7 +38,6 @@ import java.io.OutputStream
import java.nio.file.Path
import kotlin.random.Random
@Suppress("BlockingMethodInNonBlockingContext")
internal class ApkBackupTest : BackupTest() {
private val pm: PackageManager = mockk()

View file

@ -18,7 +18,7 @@ class App : Application() {
val settingsManager: SettingsManager by lazy { SettingsManager(applicationContext) }
val storageBackup: StorageBackup by lazy {
val plugin = TestSafStoragePlugin(this) { settingsManager.getBackupLocation() }
StorageBackup(this, plugin)
StorageBackup(this, { plugin })
}
override fun onCreate() {

View file

@ -36,10 +36,9 @@ import java.util.concurrent.atomic.AtomicBoolean
private const val TAG = "StorageBackup"
@Suppress("BlockingMethodInNonBlockingContext")
public class StorageBackup(
private val context: Context,
private val plugin: StoragePlugin,
private val pluginGetter: () -> StoragePlugin,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
@ -50,18 +49,18 @@ public class StorageBackup(
private val uriStore by lazy { db.getUriStore() }
private val mediaScanner by lazy { MediaScanner(context) }
private val snapshotRetriever = SnapshotRetriever(plugin)
private val chunksCacheRepopulater = ChunksCacheRepopulater(db, plugin, snapshotRetriever)
private val snapshotRetriever = SnapshotRetriever(pluginGetter)
private val chunksCacheRepopulater = ChunksCacheRepopulater(db, pluginGetter, snapshotRetriever)
private val backup by lazy {
val documentScanner = DocumentScanner(context)
val fileScanner = FileScanner(uriStore, mediaScanner, documentScanner)
Backup(context, db, fileScanner, plugin, chunksCacheRepopulater)
Backup(context, db, fileScanner, pluginGetter, chunksCacheRepopulater)
}
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 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 restoreRunning = AtomicBoolean(false)
@ -109,7 +108,7 @@ public class StorageBackup(
* (see [deleteAllSnapshots]) as well as clears local cache (see [clearCache]).
*/
public suspend fun init() {
plugin.init()
pluginGetter().init()
deleteAllSnapshots()
clearCache()
}
@ -123,9 +122,9 @@ public class StorageBackup(
*/
public suspend fun deleteAllSnapshots(): Unit = withContext(dispatcher) {
try {
plugin.getCurrentBackupSnapshots().forEach {
pluginGetter().getCurrentBackupSnapshots().forEach {
try {
plugin.deleteBackupSnapshot(it)
pluginGetter().deleteBackupSnapshot(it)
} catch (e: IOException) {
Log.e(TAG, "Error deleting snapshot $it", e)
}

View file

@ -35,12 +35,11 @@ internal class BackupResult(
)
}
@Suppress("BlockingMethodInNonBlockingContext")
internal class Backup(
private val context: Context,
private val db: Db,
private val fileScanner: FileScanner,
private val storagePlugin: StoragePlugin,
private val storagePluginGetter: () -> StoragePlugin,
private val cacheRepopulater: ChunksCacheRepopulater,
chunkSizeMax: Int = CHUNK_SIZE_MAX,
private val streamCrypto: StreamCrypto = StreamCrypto,
@ -54,6 +53,7 @@ internal class Backup(
}
private val contentResolver = context.contentResolver
private val storagePlugin get() = storagePluginGetter()
private val filesCache = db.getFilesCache()
private val chunksCache = db.getChunksCache()

View file

@ -19,10 +19,9 @@ import kotlin.time.toDuration
private const val TAG = "ChunksCacheRepopulater"
@Suppress("BlockingMethodInNonBlockingContext")
internal class ChunksCacheRepopulater(
private val db: Db,
private val storagePlugin: StoragePlugin,
private val storagePlugin: () -> StoragePlugin,
private val snapshotRetriever: SnapshotRetriever,
) {
@ -43,7 +42,7 @@ internal class ChunksCacheRepopulater(
availableChunkIds: HashSet<String>,
) {
val start = System.currentTimeMillis()
val snapshots = storagePlugin.getCurrentBackupSnapshots().mapNotNull { storedSnapshot ->
val snapshots = storagePlugin().getCurrentBackupSnapshots().mapNotNull { storedSnapshot ->
try {
snapshotRetriever.getSnapshot(streamKey, storedSnapshot)
} catch (e: GeneralSecurityException) {
@ -63,7 +62,7 @@ internal class ChunksCacheRepopulater(
// delete chunks that are not references by any snapshot anymore
val chunksToDelete = availableChunkIds.subtract(cachedChunks.map { it.id })
val deletionDuration = measure {
storagePlugin.deleteChunks(chunksToDelete.toList())
storagePlugin().deleteChunks(chunksToDelete.toList())
}
Log.i(TAG, "Deleting ${chunksToDelete.size} chunks took $deletionDuration")
}

View file

@ -14,9 +14,8 @@ import org.calyxos.backup.storage.restore.readVersion
import java.io.IOException
import java.security.GeneralSecurityException
@Suppress("BlockingMethodInNonBlockingContext")
internal class SnapshotRetriever(
private val storagePlugin: StoragePlugin,
private val storagePlugin: () -> StoragePlugin,
private val streamCrypto: StreamCrypto = StreamCrypto,
) {
@ -26,7 +25,7 @@ internal class SnapshotRetriever(
InvalidProtocolBufferException::class,
)
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 timestamp = storedSnapshot.timestamp
val ad = streamCrypto.getAssociatedDataForSnapshot(timestamp, version.toByte())

View file

@ -19,15 +19,15 @@ import kotlin.time.ExperimentalTime
private val TAG = Pruner::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class Pruner(
private val db: Db,
private val retentionManager: RetentionManager,
private val storagePlugin: StoragePlugin,
private val storagePluginGetter: () -> StoragePlugin,
private val snapshotRetriever: SnapshotRetriever,
streamCrypto: StreamCrypto = StreamCrypto,
) {
private val storagePlugin get() = storagePluginGetter()
private val chunksCache = db.getChunksCache()
private val streamKey = try {
streamCrypto.deriveStreamKey(storagePlugin.getMasterKey())

View file

@ -14,14 +14,15 @@ import java.io.InputStream
import java.io.OutputStream
import java.security.GeneralSecurityException
@Suppress("BlockingMethodInNonBlockingContext")
internal abstract class AbstractChunkRestore(
private val storagePlugin: StoragePlugin,
private val storagePluginGetter: () -> StoragePlugin,
private val fileRestore: FileRestore,
private val streamCrypto: StreamCrypto,
private val streamKey: ByteArray,
) {
private val storagePlugin get() = storagePluginGetter()
@Throws(IOException::class, GeneralSecurityException::class)
protected suspend fun getAndDecryptChunk(
version: Int,

View file

@ -24,7 +24,7 @@ private const val TAG = "MultiChunkRestore"
@Suppress("BlockingMethodInNonBlockingContext")
internal class MultiChunkRestore(
private val context: Context,
storagePlugin: StoragePlugin,
storagePlugin: () -> StoragePlugin,
fileRestore: FileRestore,
streamCrypto: StreamCrypto,
streamKey: ByteArray,

View file

@ -28,12 +28,13 @@ private const val TAG = "Restore"
internal class Restore(
context: Context,
private val storagePlugin: StoragePlugin,
private val storagePluginGetter: () -> StoragePlugin,
private val snapshotRetriever: SnapshotRetriever,
fileRestore: FileRestore,
streamCrypto: StreamCrypto = StreamCrypto,
) {
private val storagePlugin get() = storagePluginGetter()
private val streamKey by lazy {
// 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,
@ -47,13 +48,13 @@ internal class Restore(
// lazily instantiate these, so they don't try to get the streamKey too early
private val zipChunkRestore by lazy {
ZipChunkRestore(storagePlugin, fileRestore, streamCrypto, streamKey)
ZipChunkRestore(storagePluginGetter, fileRestore, streamCrypto, streamKey)
}
private val singleChunkRestore by lazy {
SingleChunkRestore(storagePlugin, fileRestore, streamCrypto, streamKey)
SingleChunkRestore(storagePluginGetter, fileRestore, streamCrypto, streamKey)
}
private val multiChunkRestore by lazy {
MultiChunkRestore(context, storagePlugin, fileRestore, streamCrypto, streamKey)
MultiChunkRestore(context, storagePluginGetter, fileRestore, streamCrypto, streamKey)
}
fun getBackupSnapshots(): Flow<SnapshotResult> = flow {

View file

@ -13,9 +13,8 @@ import org.calyxos.backup.storage.crypto.StreamCrypto
private const val TAG = "SingleChunkRestore"
@Suppress("BlockingMethodInNonBlockingContext")
internal class SingleChunkRestore(
storagePlugin: StoragePlugin,
storagePlugin: () -> StoragePlugin,
fileRestore: FileRestore,
streamCrypto: StreamCrypto,
streamKey: ByteArray,

View file

@ -17,9 +17,8 @@ import java.util.zip.ZipInputStream
private const val TAG = "ZipChunkRestore"
@Suppress("BlockingMethodInNonBlockingContext")
internal class ZipChunkRestore(
storagePlugin: StoragePlugin,
storagePlugin: () -> StoragePlugin,
fileRestore: FileRestore,
streamCrypto: StreamCrypto,
streamKey: ByteArray,

View file

@ -58,7 +58,6 @@ import java.io.OutputStream
import javax.crypto.spec.SecretKeySpec
import kotlin.random.Random
@Suppress("BlockingMethodInNonBlockingContext")
internal class BackupRestoreTest {
@get:Rule
@ -71,9 +70,10 @@ internal class BackupRestoreTest {
private val contentResolver: ContentResolver = mockk()
private val fileScanner: FileScanner = mockk()
private val pluginGetter: () -> StoragePlugin = mockk()
private val plugin: StoragePlugin = mockk()
private val fileRestore: FileRestore = mockk()
private val snapshotRetriever = SnapshotRetriever(plugin)
private val snapshotRetriever = SnapshotRetriever(pluginGetter)
private val cacheRepopulater: ChunksCacheRepopulater = mockk()
init {
@ -84,6 +84,7 @@ internal class BackupRestoreTest {
mockkStatic("org.calyxos.backup.storage.UriUtilsKt")
every { pluginGetter() } returns plugin
every { db.getFilesCache() } returns filesCache
every { db.getChunksCache() } returns chunksCache
every { plugin.getMasterKey() } returns SecretKeySpec(
@ -94,11 +95,11 @@ internal class BackupRestoreTest {
every { context.contentResolver } returns contentResolver
}
private val restore = Restore(context, plugin, snapshotRetriever, fileRestore)
private val restore = Restore(context, pluginGetter, snapshotRetriever, fileRestore)
@Test
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 smallFileM = getRandomMediaFile(smallFileMBytes.size)
@ -235,7 +236,7 @@ internal class BackupRestoreTest {
@Test
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 chunk2 = byteArrayOf(0x04, 0x05, 0x06, 0x07)

View file

@ -26,18 +26,19 @@ import org.junit.Assert.assertTrue
import org.junit.Test
import kotlin.random.Random
@Suppress("BlockingMethodInNonBlockingContext")
internal class ChunksCacheRepopulaterTest {
private val db: Db = mockk()
private val chunksCache: ChunksCache = mockk()
private val pluginGetter: () -> StoragePlugin = mockk()
private val plugin: StoragePlugin = mockk()
private val snapshotRetriever: SnapshotRetriever = mockk()
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 {
mockLog()
every { pluginGetter() } returns plugin
every { db.getChunksCache() } returns chunksCache
}

View file

@ -32,11 +32,11 @@ import org.junit.Test
import javax.crypto.spec.SecretKeySpec
import kotlin.random.Random
@Suppress("BlockingMethodInNonBlockingContext")
internal class PrunerTest {
private val db: Db = mockk()
private val chunksCache: ChunksCache = mockk()
private val pluginGetter: () -> StoragePlugin = mockk()
private val plugin: StoragePlugin = mockk()
private val snapshotRetriever: SnapshotRetriever = mockk()
private val retentionManager: RetentionManager = mockk()
@ -46,12 +46,13 @@ internal class PrunerTest {
init {
mockLog(false)
every { pluginGetter() } returns plugin
every { db.getChunksCache() } returns chunksCache
every { plugin.getMasterKey() } returns masterKey
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
fun test() = runBlocking {