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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 801b15d4..b5538aef 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -97,6 +97,15 @@
Unable to get the permission to write to the backup location.
Back
+ WebDAV Cloud (beta)
+ Integrated direct WebDAV access
+ WebDAV URL
+ User name
+ Password
+ Use WebDAV cloud
+ Invalid WebDAV URL
+ WebDAV %s
+
Recovery code
You need your 12-word recovery code to restore backed up data.