From 7e612cb8e05315ebeca7bde13b73d35b66f68b99 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 12 Apr 2024 17:47:08 -0300 Subject: [PATCH] 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. --- .../seedvault/KoinInstrumentationTestApp.kt | 9 +- .../com/stevesoltys/seedvault/PluginTest.kt | 19 +-- .../plugins/saf/DocumentsStorageTest.kt | 7 +- .../transport/backup/PackageServiceTest.kt | 2 +- .../java/com/stevesoltys/seedvault/App.kt | 41 +++++- .../seedvault/plugins/StoragePlugin.kt | 12 +- .../seedvault/plugins/StoragePluginManager.kt | 137 ++++++++++++++++++ .../saf/DocumentsProviderLegacyPlugin.kt | 5 +- .../plugins/saf/DocumentsProviderModule.kt | 18 ++- .../saf/DocumentsProviderStoragePlugin.kt | 20 +-- .../seedvault/plugins/saf/DocumentsStorage.kt | 19 +-- .../seedvault/plugins/saf/SafFactory.kt | 35 +++++ .../seedvault/plugins/saf/SafHandler.kt | 95 ++++++++++++ .../seedvault/plugins/saf/SafStorage.kt | 36 ++--- .../seedvault/plugins/webdav/WebDavModule.kt | 9 +- .../plugins/webdav/WebDavStoragePlugin.kt | 9 +- .../seedvault/restore/RestoreViewModel.kt | 4 +- .../seedvault/restore/install/ApkRestore.kt | 7 +- .../settings/BackupManagerSettings.kt | 27 ---- .../seedvault/settings/SchedulingFragment.kt | 4 +- .../seedvault/settings/SettingsFragment.kt | 16 +- .../seedvault/settings/SettingsManager.kt | 66 ++++----- .../seedvault/settings/SettingsViewModel.kt | 34 +++-- .../storage/SeedvaultSafStoragePlugin.kt | 8 +- .../stevesoltys/seedvault/storage/Services.kt | 6 +- .../seedvault/storage/StorageModule.kt | 5 +- .../transport/backup/BackupCoordinator.kt | 17 ++- .../transport/backup/BackupModule.kt | 14 +- .../seedvault/transport/backup/FullBackup.kt | 5 +- .../seedvault/transport/backup/KVBackup.kt | 8 +- .../transport/backup/PackageService.kt | 4 +- .../transport/restore/FullRestore.kt | 6 +- .../seedvault/transport/restore/KVRestore.kt | 7 +- .../transport/restore/RestoreCoordinator.kt | 10 +- .../ui/RequireProvisioningViewModel.kt | 5 +- .../ui/storage/BackupStorageViewModel.kt | 29 ++-- .../ui/storage/RestoreStorageViewModel.kt | 23 +-- .../ui/storage/StorageOptionFetcher.kt | 4 +- .../seedvault/ui/storage/StorageViewModel.kt | 100 ++----------- .../stevesoltys/seedvault/worker/ApkBackup.kt | 1 - .../seedvault/worker/ApkBackupManager.kt | 11 +- .../seedvault/worker/WorkerModule.kt | 2 +- .../java/com/stevesoltys/seedvault/TestApp.kt | 4 +- .../plugins/saf/StoragePluginTest.kt | 3 +- .../restore/install/ApkBackupRestoreTest.kt | 8 +- .../restore/install/ApkRestoreTest.kt | 19 ++- .../transport/CoordinatorIntegrationTest.kt | 22 ++- .../transport/backup/BackupCoordinatorTest.kt | 43 +++--- .../transport/backup/FullBackupTest.kt | 11 +- .../transport/backup/KVBackupTest.kt | 13 +- .../transport/restore/FullRestoreTest.kt | 17 ++- .../transport/restore/KVRestoreTest.kt | 19 ++- .../restore/RestoreCoordinatorTest.kt | 31 ++-- .../restore/RestoreV0IntegrationTest.kt | 41 +++--- .../seedvault/worker/ApkBackupManagerTest.kt | 10 +- .../seedvault/worker/ApkBackupTest.kt | 1 - .../java/de/grobox/storagebackuptester/App.kt | 2 +- .../backup/storage/api/StorageBackup.kt | 19 ++- .../calyxos/backup/storage/backup/Backup.kt | 4 +- .../storage/backup/ChunksCacheRepopulater.kt | 7 +- .../storage/plugin/SnapshotRetriever.kt | 5 +- .../calyxos/backup/storage/prune/Pruner.kt | 4 +- .../storage/restore/AbstractChunkRestore.kt | 5 +- .../storage/restore/MultiChunkRestore.kt | 2 +- .../calyxos/backup/storage/restore/Restore.kt | 9 +- .../storage/restore/SingleChunkRestore.kt | 3 +- .../backup/storage/restore/ZipChunkRestore.kt | 3 +- .../backup/storage/BackupRestoreTest.kt | 11 +- .../backup/ChunksCacheRepopulaterTest.kt | 5 +- .../backup/storage/prune/PrunerTest.kt | 5 +- 70 files changed, 720 insertions(+), 502 deletions(-) create mode 100644 app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePluginManager.kt create mode 100644 app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafFactory.kt create mode 100644 app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafHandler.kt delete mode 100644 app/src/main/java/com/stevesoltys/seedvault/settings/BackupManagerSettings.kt diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt index c00438f2..ce730a04 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt @@ -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!! } } diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt index 39135ed3..39e4be9e 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt @@ -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 = 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() diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt index 353d7680..77375cbf 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt @@ -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() - 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 diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt index ca54566d..d49ea9c6 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt @@ -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() { diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index 372c86fc..385985d6 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -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.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, diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePlugin.kt index b7519cae..f5109add 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePlugin.kt @@ -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 { /** * 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. * diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePluginManager.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePluginManager.kt new file mode 100644 index 00000000..b69d8af4 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePluginManager.kt @@ -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 { + 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 changePlugins( + storageProperties: StorageProperties, + appPlugin: StoragePlugin, + 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) + } + +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderLegacyPlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderLegacyPlugin.kt index 1a1d075f..e5f4eb64 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderLegacyPlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderLegacyPlugin.kt @@ -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? = null diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderModule.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderModule.kt index 638ee990..4c6590e3 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderModule.kt @@ -1,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 { DocumentsProviderStoragePlugin(androidContext(), get()) } @Suppress("Deprecation") - single { DocumentsProviderLegacyPlugin(androidContext(), get()) } + single { + DocumentsProviderLegacyPlugin( + context = androidContext(), + storageGetter = { + val safStorage = get().getSafStorage() ?: error("No SAF storage") + DocumentsStorage(androidContext(), get(), safStorage) + }, + ) + } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt index 9f3dab51..e1d88f54 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt @@ -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 { /** * 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? { 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 { val backupSets = ArrayList() val files = try { diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt index e96b29e2..ce1b7dd3 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt @@ -1,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? { diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafFactory.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafFactory.kt new file mode 100644 index 00000000..af46f6aa --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafFactory.kt @@ -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 { + 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) + } + +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafHandler.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafHandler.kt new file mode 100644 index 00000000..3b8dc78c --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafHandler.kt @@ -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), + ) + } +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafStorage.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafStorage.kt index 5b9cdfb8..35d58fba 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafStorage.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/SafStorage.kt @@ -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() { + + 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) - } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavModule.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavModule.kt index 8126e48e..9273823b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavModule.kt @@ -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 { WebDavStoragePlugin(androidContext(), WebDavConfig("", "", "")) } - - single { DocumentsStorage(androidContext(), get()) } - @Suppress("Deprecation") - single { DocumentsProviderLegacyPlugin(androidContext(), get()) } + single> { WebDavStoragePlugin(androidContext(), WebDavConfig("", "", "")) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt index 075d7f87..0d9986cd 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavStoragePlugin.kt @@ -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 { 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? { return try { doGetAvailableBackups() diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt index c7eaee3a..cdcfe0a5 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt @@ -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 diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt index 36326e0a..aa9e5f30 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt @@ -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, @@ -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, diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/BackupManagerSettings.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/BackupManagerSettings.kt deleted file mode 100644 index 622693c1..00000000 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/BackupManagerSettings.kt +++ /dev/null @@ -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) - } - } - -} diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SchedulingFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SchedulingFragment.kt index f1bcdc39..3837c112 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SchedulingFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SchedulingFragment.kt @@ -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("scheduling_category_conditions")?.isEnabled = false } diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt index 7d821147..58832235 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt @@ -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 } diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt index dfaf42da..53891a40 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -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?, diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index 2fd78d1f..f247e24e 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -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,24 +152,27 @@ 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") - // 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. - Log.e(TAG, "Error registering content observer for ${storage.uri}", e) + 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. + Log.e(TAG, "Error registering content observer for ${storage.uri}", e) + } } // register network observer if needed @@ -301,7 +307,7 @@ internal class SettingsViewModel( } } - fun cancelAppBackup() { + private fun cancelAppBackup() { AppBackupWorker.unschedule(app) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/storage/SeedvaultSafStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/storage/SeedvaultSafStoragePlugin.kt index e78dd907..7a3e8a1f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/storage/SeedvaultSafStoragePlugin.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/storage/SeedvaultSafStoragePlugin.kt @@ -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() diff --git a/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt b/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt index f7b0c828..3eba826d 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/storage/Services.kt @@ -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) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/storage/StorageModule.kt b/app/src/main/java/com/stevesoltys/seedvault/storage/StorageModule.kt index 223ccca3..11b8b593 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/storage/StorageModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/storage/StorageModule.kt @@ -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 { SeedvaultSafStoragePlugin(get(), get(), get()) } - single { StorageBackup(get(), get()) } + single { StorageBackup(get(), { get().filesPlugin }) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt index 8bf4d4ba..d0358877 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt @@ -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) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt index 6bb6f6e2..6128c1e7 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt @@ -11,38 +11,38 @@ val backupModule = module { context = androidContext(), backupManager = get(), settingsManager = get(), - plugin = get() + pluginManager = get(), ) } single { 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(), ) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt index d44cdebf..5b76abb5 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt @@ -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 diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt index 44678157..c867144f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt @@ -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 diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt index 2aa126cc..ac803769 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt @@ -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 @WorkerThread diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt index 91c83e34..76800762 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt @@ -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 diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt index c529c0ad..6b3e9618 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt @@ -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 /** diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt index 1111a750..147907d1 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt @@ -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() @@ -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) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/RequireProvisioningViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/RequireProvisioningViewModel.kt index 28064441..5dd6faf7 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/RequireProvisioningViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/RequireProvisioningViewModel.kt @@ -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 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() diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt index 76f08757..455dc780 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt @@ -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) { - // remove old storage snapshots and clear cache - // TODO is this needed? It also does create all 255 chunk folders which takes time - // pass a flag to getCurrentBackupSnapshots() to not create missing folders? - storageBackup.init() try { + // remove old storage snapshots and clear cache + // TODO For SAF, this also does create all 255 chunk folders which takes time + // pass a flag to getCurrentBackupSnapshots() to not create missing folders? + storageBackup.init() // initialize the new location (if backups are enabled) 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) diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt index b1ea418e..a938176a 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt @@ -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( } } } - } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageOptionFetcher.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageOptionFetcher.kt index afe35a00..5ca55039 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageOptionFetcher.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageOptionFetcher.kt @@ -62,9 +62,7 @@ internal class StorageOptionFetcher(private val context: Context, private val is internal fun getRemovableStorageListener() = listener internal fun getStorageOptions(): List { - val roots = ArrayList().apply { - add(WebDavOption(context)) - } + val roots = ArrayList() val intent = Intent(PROVIDER_INTERFACE) val providers = packageManager.queryIntentContentProviders(intent, 0) for (info in providers) { diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageViewModel.kt index b3673d2a..2be75a0b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageViewModel.kt @@ -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>() @@ -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) diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackup.kt index 1f39c3c0..991039ad 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackup.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackup.kt @@ -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, diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackupManager.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackupManager.kt index be1942c1..00f707e0 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackupManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackupManager.kt @@ -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) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt index dce45be2..b7ff36de 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt @@ -31,7 +31,7 @@ val workerModule = module { metadataManager = get(), packageService = get(), apkBackup = get(), - plugin = get(), + pluginManager = get(), nm = get() ) } diff --git a/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt b/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt index 1ad185aa..724ba41e 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt @@ -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, diff --git a/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/StoragePluginTest.kt b/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/StoragePluginTest.kt index d06004fd..982124de 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/StoragePluginTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/plugins/saf/StoragePluginTest.kt @@ -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() @@ -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) diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt index 33a244aa..c22c4a38 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt @@ -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 diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt index 6eb1e969..e6017adc 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt @@ -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().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 diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt index 94d3ea8c..70b8b35d 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt @@ -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() @@ -58,18 +58,20 @@ internal class CoordinatorIntegrationTest : TransportTest() { private val metadataReader = MetadataReaderImpl(cryptoImpl) private val notificationManager = mockk() private val dbManager = TestKvDbManager() + private val storagePluginManager: StoragePluginManager = mockk() @Suppress("Deprecation") private val legacyPlugin = mockk() - private val backupPlugin = mockk() + private val backupPlugin = mockk>() 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() 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() diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt index 9d39cf2c..eeb48ded 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt @@ -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() + private val pluginManager = mockk() private val kv = mockk() private val full = mockk() private val apkBackup = mockk() @@ -44,27 +44,32 @@ internal class BackupCoordinatorTest : BackupTest() { private val packageService = mockk() 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>() private val metadataOutputStream = mockk() 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 diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt index 2d03dd7e..1681b6f5 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt @@ -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() - private val backup = FullBackup(plugin, settingsManager, inputFactory, crypto) + private val storagePluginManager: StoragePluginManager = mockk() + private val plugin = mockk>() + private val backup = FullBackup(storagePluginManager, settingsManager, inputFactory, crypto) private val bytes = ByteArray(23).apply { Random.nextBytes(this) } private val inputStream = mockk() private val ad = getADForFull(VERSION, packageInfo.packageName) + init { + every { storagePluginManager.appPlugin } returns plugin + } + @Test fun `has no initial state`() { assertFalse(backup.hasState()) diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt index e370f080..f9bbe264 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt @@ -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() + private val pluginManager = mockk() private val dataInput = mockk() private val dbManager = mockk() - private val backup = KVBackup(plugin, settingsManager, inputFactory, crypto, dbManager) + private val backup = KVBackup(pluginManager, settingsManager, inputFactory, crypto, dbManager) private val db = mockk() + private val plugin = mockk>() 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)) diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt index 73044e0d..3986c069 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt @@ -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() + private val storagePluginManager: StoragePluginManager = mockk() + private val plugin = mockk>() private val legacyPlugin = mockk() - 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()) diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt index 678d6b63..955d0c6b 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt @@ -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() + private val storagePluginManager: StoragePluginManager = mockk() + private val plugin = mockk>() + @Suppress("DEPRECATION") private val legacyPlugin = mockk() private val dbManager = mockk() private val output = mockk() - 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() 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) diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt index f752ad72..9b1e609e 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt @@ -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() + private val storagePluginManager: StoragePluginManager = mockk() + private val plugin = mockk>() private val kv = mockk() private val full = mockk() private val metadataReader = mockk() 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() @@ -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 diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt index 07df8f34..d38ee2f9 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt @@ -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() @@ -49,30 +49,31 @@ internal class RestoreV0IntegrationTest : TransportTest() { private val dbManager = mockk() private val metadataReader = MetadataReaderImpl(cryptoImpl) private val notificationManager = mockk() + private val storagePluginManager: StoragePluginManager = mockk() @Suppress("Deprecation") private val legacyPlugin = mockk() - private val backupPlugin = mockk() + private val backupPlugin = mockk>() 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(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" + diff --git a/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt index f12f4def..d38ae1d6 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt @@ -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() 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 diff --git a/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupTest.kt index c56fcd24..c79f007b 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupTest.kt @@ -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() diff --git a/storage/demo/src/main/java/de/grobox/storagebackuptester/App.kt b/storage/demo/src/main/java/de/grobox/storagebackuptester/App.kt index 6aaae909..11bfebd9 100644 --- a/storage/demo/src/main/java/de/grobox/storagebackuptester/App.kt +++ b/storage/demo/src/main/java/de/grobox/storagebackuptester/App.kt @@ -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() { diff --git a/storage/lib/src/main/java/org/calyxos/backup/storage/api/StorageBackup.kt b/storage/lib/src/main/java/org/calyxos/backup/storage/api/StorageBackup.kt index d5975978..9b5e1252 100644 --- a/storage/lib/src/main/java/org/calyxos/backup/storage/api/StorageBackup.kt +++ b/storage/lib/src/main/java/org/calyxos/backup/storage/api/StorageBackup.kt @@ -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) } diff --git a/storage/lib/src/main/java/org/calyxos/backup/storage/backup/Backup.kt b/storage/lib/src/main/java/org/calyxos/backup/storage/backup/Backup.kt index d1451f2b..167f78f5 100644 --- a/storage/lib/src/main/java/org/calyxos/backup/storage/backup/Backup.kt +++ b/storage/lib/src/main/java/org/calyxos/backup/storage/backup/Backup.kt @@ -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() diff --git a/storage/lib/src/main/java/org/calyxos/backup/storage/backup/ChunksCacheRepopulater.kt b/storage/lib/src/main/java/org/calyxos/backup/storage/backup/ChunksCacheRepopulater.kt index b29dd346..b29ca369 100644 --- a/storage/lib/src/main/java/org/calyxos/backup/storage/backup/ChunksCacheRepopulater.kt +++ b/storage/lib/src/main/java/org/calyxos/backup/storage/backup/ChunksCacheRepopulater.kt @@ -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, ) { 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") } diff --git a/storage/lib/src/main/java/org/calyxos/backup/storage/plugin/SnapshotRetriever.kt b/storage/lib/src/main/java/org/calyxos/backup/storage/plugin/SnapshotRetriever.kt index 3137b444..0eb63764 100644 --- a/storage/lib/src/main/java/org/calyxos/backup/storage/plugin/SnapshotRetriever.kt +++ b/storage/lib/src/main/java/org/calyxos/backup/storage/plugin/SnapshotRetriever.kt @@ -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()) diff --git a/storage/lib/src/main/java/org/calyxos/backup/storage/prune/Pruner.kt b/storage/lib/src/main/java/org/calyxos/backup/storage/prune/Pruner.kt index e25f0b76..c6c91f35 100644 --- a/storage/lib/src/main/java/org/calyxos/backup/storage/prune/Pruner.kt +++ b/storage/lib/src/main/java/org/calyxos/backup/storage/prune/Pruner.kt @@ -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()) diff --git a/storage/lib/src/main/java/org/calyxos/backup/storage/restore/AbstractChunkRestore.kt b/storage/lib/src/main/java/org/calyxos/backup/storage/restore/AbstractChunkRestore.kt index 5871d222..9c207629 100644 --- a/storage/lib/src/main/java/org/calyxos/backup/storage/restore/AbstractChunkRestore.kt +++ b/storage/lib/src/main/java/org/calyxos/backup/storage/restore/AbstractChunkRestore.kt @@ -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, diff --git a/storage/lib/src/main/java/org/calyxos/backup/storage/restore/MultiChunkRestore.kt b/storage/lib/src/main/java/org/calyxos/backup/storage/restore/MultiChunkRestore.kt index 16fc96f5..2b5f8e6d 100644 --- a/storage/lib/src/main/java/org/calyxos/backup/storage/restore/MultiChunkRestore.kt +++ b/storage/lib/src/main/java/org/calyxos/backup/storage/restore/MultiChunkRestore.kt @@ -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, diff --git a/storage/lib/src/main/java/org/calyxos/backup/storage/restore/Restore.kt b/storage/lib/src/main/java/org/calyxos/backup/storage/restore/Restore.kt index 16776135..e5d2ca4f 100644 --- a/storage/lib/src/main/java/org/calyxos/backup/storage/restore/Restore.kt +++ b/storage/lib/src/main/java/org/calyxos/backup/storage/restore/Restore.kt @@ -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 = flow { diff --git a/storage/lib/src/main/java/org/calyxos/backup/storage/restore/SingleChunkRestore.kt b/storage/lib/src/main/java/org/calyxos/backup/storage/restore/SingleChunkRestore.kt index 75d4d3bc..a9fa530b 100644 --- a/storage/lib/src/main/java/org/calyxos/backup/storage/restore/SingleChunkRestore.kt +++ b/storage/lib/src/main/java/org/calyxos/backup/storage/restore/SingleChunkRestore.kt @@ -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, diff --git a/storage/lib/src/main/java/org/calyxos/backup/storage/restore/ZipChunkRestore.kt b/storage/lib/src/main/java/org/calyxos/backup/storage/restore/ZipChunkRestore.kt index c6c995b0..608668da 100644 --- a/storage/lib/src/main/java/org/calyxos/backup/storage/restore/ZipChunkRestore.kt +++ b/storage/lib/src/main/java/org/calyxos/backup/storage/restore/ZipChunkRestore.kt @@ -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, diff --git a/storage/lib/src/test/java/org/calyxos/backup/storage/BackupRestoreTest.kt b/storage/lib/src/test/java/org/calyxos/backup/storage/BackupRestoreTest.kt index 0f93d35e..dd53e98c 100644 --- a/storage/lib/src/test/java/org/calyxos/backup/storage/BackupRestoreTest.kt +++ b/storage/lib/src/test/java/org/calyxos/backup/storage/BackupRestoreTest.kt @@ -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) diff --git a/storage/lib/src/test/java/org/calyxos/backup/storage/backup/ChunksCacheRepopulaterTest.kt b/storage/lib/src/test/java/org/calyxos/backup/storage/backup/ChunksCacheRepopulaterTest.kt index 51c3a554..eb668d3b 100644 --- a/storage/lib/src/test/java/org/calyxos/backup/storage/backup/ChunksCacheRepopulaterTest.kt +++ b/storage/lib/src/test/java/org/calyxos/backup/storage/backup/ChunksCacheRepopulaterTest.kt @@ -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 } diff --git a/storage/lib/src/test/java/org/calyxos/backup/storage/prune/PrunerTest.kt b/storage/lib/src/test/java/org/calyxos/backup/storage/prune/PrunerTest.kt index f9a2c478..7da12a29 100644 --- a/storage/lib/src/test/java/org/calyxos/backup/storage/prune/PrunerTest.kt +++ b/storage/lib/src/test/java/org/calyxos/backup/storage/prune/PrunerTest.kt @@ -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 {