diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index da3bafa2..a24ebdac 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -100,7 +100,8 @@ + android:theme="@style/AppTheme.NoActionBar" + android:windowSoftInputMode="adjustResize" /> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) } factory { AppListRetriever(this@App, get(), get(), get()) } @@ -74,9 +75,6 @@ open class App : Application() { ) } viewModel { RecoveryCodeViewModel(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, @@ -84,11 +82,12 @@ open class App : Application() { backupInitializer = get(), storageBackup = get(), safHandler = get(), + webDavHandler = get(), settingsManager = get(), storagePluginManager = get(), ) } - viewModel { RestoreStorageViewModel(this@App, get(), get(), get()) } + viewModel { RestoreStorageViewModel(this@App, get(), get(), get(), get()) } viewModel { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get(), get()) } viewModel { FileSelectionViewModel(this@App, get()) } } @@ -127,7 +126,8 @@ open class App : Application() { cryptoModule, headerModule, metadataModule, - storagePluginModuleSaf, // storage plugin + storagePluginModuleSaf, + storagePluginModuleWebDav, backupModule, restoreModule, installModule, diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePluginManager.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePluginManager.kt index b69d8af4..ce2e1b58 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePluginManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/StoragePluginManager.kt @@ -14,6 +14,7 @@ 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.plugins.webdav.WebDavFactory import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.StoragePluginEnum @@ -47,6 +48,7 @@ class StoragePluginManager( private val context: Context, private val settingsManager: SettingsManager, safFactory: SafFactory, + webDavFactory: WebDavFactory, ) { private var _appPlugin: StoragePlugin<*>? @@ -81,6 +83,14 @@ class StoragePluginManager( _storageProperties = safStorage } + StoragePluginEnum.WEB_DAV -> { + val webDavProperties = + settingsManager.webDavProperties ?: error("No WebDAV config saved") + _appPlugin = webDavFactory.createAppStoragePlugin(webDavProperties.config) + _filesPlugin = webDavFactory.createFilesStoragePlugin(webDavProperties.config) + _storageProperties = webDavProperties + } + null -> { _appPlugin = null _filesPlugin = null diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavFactory.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavFactory.kt new file mode 100644 index 00000000..f11728e1 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavFactory.kt @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.plugins.webdav + +import android.annotation.SuppressLint +import android.content.Context +import android.provider.Settings +import com.stevesoltys.seedvault.crypto.KeyManager +import com.stevesoltys.seedvault.plugins.StoragePlugin + +class WebDavFactory( + private val context: Context, + private val keyManager: KeyManager, +) { + + fun createAppStoragePlugin(config: WebDavConfig): StoragePlugin { + return WebDavStoragePlugin(context, config) + } + + fun createFilesStoragePlugin( + config: WebDavConfig, + ): org.calyxos.backup.storage.api.StoragePlugin { + @SuppressLint("HardwareIds") + val androidId = + Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) + return com.stevesoltys.seedvault.storage.WebDavStoragePlugin( + keyManager = keyManager, + androidId = androidId, + webDavConfig = config, + ) + } + +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavHandler.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavHandler.kt new file mode 100644 index 00000000..d471901a --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavHandler.kt @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.plugins.webdav + +import android.content.Context +import android.util.Log +import androidx.annotation.WorkerThread +import com.stevesoltys.seedvault.R +import com.stevesoltys.seedvault.plugins.StoragePluginManager +import com.stevesoltys.seedvault.settings.SettingsManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import okhttp3.HttpUrl.Companion.toHttpUrl +import java.io.IOException + +internal sealed interface WebDavConfigState { + object Empty : WebDavConfigState + object Checking : WebDavConfigState + class Success( + val properties: WebDavProperties, + val plugin: WebDavStoragePlugin, + ) : WebDavConfigState + + class Error(val e: Exception?) : WebDavConfigState +} + +private val TAG = WebDavHandler::class.java.simpleName + +internal class WebDavHandler( + private val context: Context, + private val webDavFactory: WebDavFactory, + private val settingsManager: SettingsManager, + private val storagePluginManager: StoragePluginManager, +) { + + companion object { + fun createWebDavProperties(context: Context, config: WebDavConfig): WebDavProperties { + val host = config.url.toHttpUrl().host + return WebDavProperties( + config = config, + name = context.getString(R.string.storage_webdav_name, host), + ) + } + } + + private val _configState = MutableStateFlow(WebDavConfigState.Empty) + val configState = _configState.asStateFlow() + + suspend fun onConfigReceived(config: WebDavConfig) { + _configState.value = WebDavConfigState.Checking + val plugin = webDavFactory.createAppStoragePlugin(config) as WebDavStoragePlugin + try { + if (plugin.test()) { + val properties = createWebDavProperties(context, config) + _configState.value = WebDavConfigState.Success(properties, plugin) + } else { + _configState.value = WebDavConfigState.Error(null) + } + } catch (e: Exception) { + Log.e(TAG, "Error testing WebDAV config at ${config.url}", e) + _configState.value = WebDavConfigState.Error(e) + } + } + + /** + * 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(appPlugin: WebDavStoragePlugin): Boolean { + val backups = appPlugin.getAvailableBackups() + return backups != null && backups.iterator().hasNext() + } + + fun save(properties: WebDavProperties) { + settingsManager.saveWebDavConfig(properties.config) + } + + fun setPlugin(properties: WebDavProperties, plugin: WebDavStoragePlugin) { + storagePluginManager.changePlugins( + storageProperties = properties, + appPlugin = plugin, + filesPlugin = webDavFactory.createFilesStoragePlugin(properties.config), + ) + } + +} 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 9273823b..807c976a 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,10 +1,9 @@ package com.stevesoltys.seedvault.plugins.webdav -import com.stevesoltys.seedvault.plugins.StoragePlugin 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("", "", "")) } +val storagePluginModuleWebDav = module { + single { WebDavFactory(androidContext(), get()) } + single { WebDavHandler(androidContext(), get(), get(), get()) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavProperties.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavProperties.kt new file mode 100644 index 00000000..29ea84a9 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/webdav/WebDavProperties.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.plugins.webdav + +import android.content.Context +import com.stevesoltys.seedvault.plugins.StorageProperties + +data class WebDavProperties( + override val config: WebDavConfig, + override val name: String, +) : StorageProperties() { + override val isUsb: Boolean = false + override val requiresNetwork: Boolean = true + override fun isUnavailableUsb(context: Context): Boolean = false +} 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 53891a40..81251c47 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -5,13 +5,15 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.hardware.usb.UsbDevice import android.net.Uri import androidx.annotation.UiThread -import androidx.annotation.WorkerThread 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.plugins.webdav.WebDavConfig +import com.stevesoltys.seedvault.plugins.webdav.WebDavHandler.Companion.createWebDavProperties +import com.stevesoltys.seedvault.plugins.webdav.WebDavProperties +import com.stevesoltys.seedvault.plugins.webdav.WebDavStoragePlugin import com.stevesoltys.seedvault.transport.backup.BackupCoordinator import java.util.concurrent.ConcurrentSkipListSet @@ -26,6 +28,7 @@ private const val PREF_KEY_STORAGE_PLUGIN = "storagePlugin" internal enum class StoragePluginEnum { // don't rename, will break existing installs SAF, + WEB_DAV, } private const val PREF_KEY_STORAGE_URI = "storageUri" @@ -38,6 +41,10 @@ private const val PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER = "flashSerialNumber" private const val PREF_KEY_FLASH_DRIVE_VENDOR_ID = "flashDriveVendorId" private const val PREF_KEY_FLASH_DRIVE_PRODUCT_ID = "flashDriveProductId" +private const val PREF_KEY_WEBDAV_URL = "webDavUrl" +private const val PREF_KEY_WEBDAV_USER = "webDavUser" +private const val PREF_KEY_WEBDAV_PASS = "webDavPass" + private const val PREF_KEY_BACKUP_APP_BLACKLIST = "backupAppBlacklist" private const val PREF_KEY_BACKUP_STORAGE = "backup_storage" @@ -94,7 +101,6 @@ class SettingsManager(private val context: Context) { token = newToken } - // 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 { @@ -107,6 +113,7 @@ class SettingsManager(private val context: Context) { fun setStoragePlugin(plugin: StoragePlugin<*>) { val value = when (plugin) { is DocumentsProviderStoragePlugin -> StoragePluginEnum.SAF + is WebDavStoragePlugin -> StoragePluginEnum.WEB_DAV else -> error("Unsupported plugin: ${plugin::class.java.simpleName}") }.name prefs.edit() @@ -159,20 +166,22 @@ class SettingsManager(private val context: Context) { return FlashDrive(name, serialNumber, vendorId, productId) } - /** - * 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 = getSafStorage() ?: return false - val systemContext = context.getStorageContext { storage.isUsb } - return !storage.isUnavailableUsb(systemContext) && - !storage.isUnavailableNetwork(context, useMeteredNetwork) + val webDavProperties: WebDavProperties? + get() { + val config = WebDavConfig( + url = prefs.getString(PREF_KEY_WEBDAV_URL, null) ?: return null, + username = prefs.getString(PREF_KEY_WEBDAV_USER, null) ?: return null, + password = prefs.getString(PREF_KEY_WEBDAV_PASS, null) ?: return null, + ) + return createWebDavProperties(context, config) + } + + fun saveWebDavConfig(config: WebDavConfig) { + prefs.edit() + .putString(PREF_KEY_WEBDAV_URL, config.url) + .putString(PREF_KEY_WEBDAV_USER, config.username) + .putString(PREF_KEY_WEBDAV_PASS, config.password) + .apply() } fun backupApks(): Boolean { 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 455dc780..8cf4ce8b 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 @@ -10,6 +10,9 @@ 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.plugins.webdav.WebDavHandler +import com.stevesoltys.seedvault.plugins.webdav.WebDavProperties +import com.stevesoltys.seedvault.plugins.webdav.WebDavStoragePlugin import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.storage.StorageBackupJobService import com.stevesoltys.seedvault.transport.backup.BackupInitializer @@ -29,9 +32,10 @@ internal class BackupStorageViewModel( private val backupInitializer: BackupInitializer, private val storageBackup: StorageBackup, safHandler: SafHandler, + webDavHandler: WebDavHandler, settingsManager: SettingsManager, storagePluginManager: StoragePluginManager, -) : StorageViewModel(app, safHandler, settingsManager, storagePluginManager) { +) : StorageViewModel(app, safHandler, webDavHandler, settingsManager, storagePluginManager) { override val isRestoreOperation = false @@ -49,6 +53,13 @@ internal class BackupStorageViewModel( onStorageLocationSet(safStorage.isUsb) } + override fun onWebDavConfigSet(properties: WebDavProperties, plugin: WebDavStoragePlugin) { + webdavHandler.save(properties) + webdavHandler.setPlugin(properties, plugin) + scheduleBackupWorkers() + onStorageLocationSet(isUsb = false) + } + private fun onStorageLocationSet(isUsb: Boolean) { viewModelScope.launch(Dispatchers.IO) { try { 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 a938176a..179c7ae9 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 @@ -8,6 +8,9 @@ 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.plugins.webdav.WebDavHandler +import com.stevesoltys.seedvault.plugins.webdav.WebDavProperties +import com.stevesoltys.seedvault.plugins.webdav.WebDavStoragePlugin import com.stevesoltys.seedvault.settings.SettingsManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -18,9 +21,10 @@ private val TAG = RestoreStorageViewModel::class.java.simpleName internal class RestoreStorageViewModel( private val app: Application, safHandler: SafHandler, + webDavHandler: WebDavHandler, settingsManager: SettingsManager, storagePluginManager: StoragePluginManager, -) : StorageViewModel(app, safHandler, settingsManager, storagePluginManager) { +) : StorageViewModel(app, safHandler, webDavHandler, settingsManager, storagePluginManager) { override val isRestoreOperation = true @@ -46,4 +50,27 @@ internal class RestoreStorageViewModel( } } } + + override fun onWebDavConfigSet(properties: WebDavProperties, plugin: WebDavStoragePlugin) { + viewModelScope.launch(Dispatchers.IO) { + val hasBackup = try { + webdavHandler.hasAppBackup(plugin) + } catch (e: IOException) { + Log.e(TAG, "Error reading: ${properties.config.url}", e) + false + } + if (hasBackup) { + webdavHandler.save(properties) + webdavHandler.setPlugin(properties, plugin) + mLocationChecked.postEvent(LocationResult()) + } else { + Log.w(TAG, "Location was rejected: ${properties.config.url}") + + // notify the UI that the location was invalid + val errorMsg = + app.getString(R.string.restore_invalid_location_message, DIRECTORY_ROOT) + mLocationChecked.postEvent(LocationResult(errorMsg)) + } + } + } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageOption.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageOption.kt index c7cc0bc1..7fe477e0 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageOption.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageOption.kt @@ -1,8 +1,11 @@ package com.stevesoltys.seedvault.ui.storage +import android.content.Context import android.graphics.drawable.Drawable import android.net.Uri import android.provider.DocumentsContract.buildTreeDocumentUri +import androidx.appcompat.content.res.AppCompatResources.getDrawable +import com.stevesoltys.seedvault.R internal sealed class StorageOption { abstract val id: String @@ -46,3 +49,14 @@ internal sealed class StorageOption { return id.hashCode() } } + +internal class WebDavOption(context: Context) : StorageOption() { + override val id: String = "webdav" + override val icon: Drawable? = getDrawable(context, R.drawable.ic_cloud_circle) + override val title: String = context.getString(R.string.storage_webdav_option_title) + override val summary: String = context.getString(R.string.storage_webdav_option_summary) + override val availableBytes: Long? = null + override val requiresNetwork: Boolean = true + override val enabled: Boolean = true + override val nonDefaultAction: (() -> Unit)? = null +} 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 5ca55039..afe35a00 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,7 +62,9 @@ internal class StorageOptionFetcher(private val context: Context, private val is internal fun getRemovableStorageListener() = listener internal fun getStorageOptions(): List { - val roots = ArrayList() + val roots = ArrayList().apply { + add(WebDavOption(context)) + } 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/StorageOptionsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageOptionsFragment.kt index 66f3b78b..fa1f7e18 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageOptionsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageOptionsFragment.kt @@ -115,11 +115,21 @@ internal class StorageOptionsFragment : Fragment(), StorageOptionClickedListener } override fun onClick(storageOption: StorageOption) { - if (storageOption is SafOption) { - viewModel.onSafOptionChosen(storageOption) - openDocumentTree.launch(storageOption.uri) - } else { - throw IllegalArgumentException("Non-SAF storage not yet supported") + when (storageOption) { + is SafOption -> { + viewModel.onSafOptionChosen(storageOption) + openDocumentTree.launch(storageOption.uri) + } + + is WebDavOption -> { + val isRestore = requireArguments().getBoolean(INTENT_EXTRA_IS_RESTORE) + val f = WebDavConfigFragment.newInstance(isRestore) + parentFragmentManager.beginTransaction().apply { + replace(R.id.fragment, f) + addToBackStack("WebDAV") + commit() + } + } } } 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 2be75a0b..fe5b7a8a 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 @@ -11,6 +11,10 @@ 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.plugins.webdav.WebDavConfig +import com.stevesoltys.seedvault.plugins.webdav.WebDavHandler +import com.stevesoltys.seedvault.plugins.webdav.WebDavProperties +import com.stevesoltys.seedvault.plugins.webdav.WebDavStoragePlugin import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.ui.LiveEvent import com.stevesoltys.seedvault.ui.MutableLiveEvent @@ -21,6 +25,7 @@ import kotlinx.coroutines.launch internal abstract class StorageViewModel( private val app: Application, protected val safHandler: SafHandler, + protected val webdavHandler: WebDavHandler, protected val settingsManager: SettingsManager, protected val storagePluginManager: StoragePluginManager, ) : AndroidViewModel(app), RemovableStorageListener { @@ -79,11 +84,26 @@ internal abstract class StorageViewModel( } abstract fun onSafUriSet(safStorage: SafStorage) + abstract fun onWebDavConfigSet(properties: WebDavProperties, plugin: WebDavStoragePlugin) override fun onCleared() { storageOptionFetcher.setRemovableStorageListener(null) super.onCleared() } + val webdavConfigState get() = webdavHandler.configState + + fun onWebDavConfigReceived(url: String, user: String, pass: String) { + val config = WebDavConfig(url = url, username = user, password = pass) + viewModelScope.launch(Dispatchers.IO) { + webdavHandler.onConfigReceived(config) + } + } + + @UiThread + fun onWebDavConfigSuccess(properties: WebDavProperties, plugin: WebDavStoragePlugin) { + mLocationSet.setEvent(true) + onWebDavConfigSet(properties, plugin) + } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/WebDavConfigFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/WebDavConfigFragment.kt new file mode 100644 index 00000000..90f502df --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/WebDavConfigFragment.kt @@ -0,0 +1,128 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.ui.storage + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.INVISIBLE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.widget.Button +import android.widget.ProgressBar +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle.State.STARTED +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.transition.TransitionManager.beginDelayedTransition +import com.google.android.material.snackbar.Snackbar +import com.google.android.material.snackbar.Snackbar.LENGTH_LONG +import com.google.android.material.textfield.TextInputEditText +import com.stevesoltys.seedvault.R +import com.stevesoltys.seedvault.plugins.webdav.WebDavConfigState +import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE +import kotlinx.coroutines.launch +import org.koin.androidx.viewmodel.ext.android.getSharedViewModel + +class WebDavConfigFragment : Fragment(), View.OnClickListener { + + companion object { + fun newInstance(isRestore: Boolean): WebDavConfigFragment { + val f = WebDavConfigFragment() + f.arguments = Bundle().apply { + putBoolean(INTENT_EXTRA_IS_RESTORE, isRestore) + } + return f + } + } + + private lateinit var viewModel: StorageViewModel + + private lateinit var urlInput: TextInputEditText + private lateinit var userInput: TextInputEditText + private lateinit var passInput: TextInputEditText + private lateinit var button: Button + private lateinit var progressBar: ProgressBar + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + val v: View = inflater.inflate(R.layout.fragment_webdav_config, container, false) + urlInput = v.requireViewById(R.id.webdavUrlInput) + userInput = v.requireViewById(R.id.webdavUserInput) + passInput = v.requireViewById(R.id.webDavPassInput) + button = v.requireViewById(R.id.webdavButton) + button.setOnClickListener(this) + progressBar = v.requireViewById(R.id.progressBar) + return v + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel = if (requireArguments().getBoolean(INTENT_EXTRA_IS_RESTORE)) { + getSharedViewModel() + } else { + getSharedViewModel() + } + lifecycleScope.launch { + viewModel.webdavConfigState.flowWithLifecycle(lifecycle, STARTED).collect { + onConfigStateChanged(it) + } + } + } + + override fun onClick(v: View) { + if (urlInput.text.isNullOrBlank()) { + Snackbar.make( + requireView(), + R.string.storage_webdav_config_malformed_url, + LENGTH_LONG + ).setAnchorView(button).show() + } else { + viewModel.onWebDavConfigReceived( + url = urlInput.text.toString(), + user = userInput.text.toString(), + pass = passInput.text.toString(), + ) + } + } + + private fun onConfigStateChanged(state: WebDavConfigState) { + when (state) { + WebDavConfigState.Empty -> { + + } + + WebDavConfigState.Checking -> { + beginDelayedTransition(requireView() as ViewGroup) + progressBar.visibility = VISIBLE + button.visibility = INVISIBLE + } + + is WebDavConfigState.Success -> { + viewModel.onWebDavConfigSuccess(state.properties, state.plugin) + } + + is WebDavConfigState.Error -> { + val s = if (state.e == null) { + getString(R.string.storage_check_fragment_backup_error) + } else { + getString(R.string.storage_check_fragment_backup_error) + + " ${state.e::class.java.simpleName} ${state.e.message}" + } + Snackbar.make(requireView(), s, LENGTH_LONG).setAnchorView(button).show() + + beginDelayedTransition(requireView() as ViewGroup) + progressBar.visibility = INVISIBLE + button.visibility = VISIBLE + } + } + } + +} diff --git a/app/src/main/res/drawable/ic_cloud_circle.xml b/app/src/main/res/drawable/ic_cloud_circle.xml new file mode 100644 index 00000000..9bb47d6a --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud_circle.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_webdav_config.xml b/app/src/main/res/layout/fragment_webdav_config.xml new file mode 100644 index 00000000..f6272ef1 --- /dev/null +++ b/app/src/main/res/layout/fragment_webdav_config.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + +