Add the UI for the WebDAV plugin

including ViewModel and StoragePluginManager logic
This commit is contained in:
Torsten Grote 2024-04-12 17:54:55 -03:00
parent 7e612cb8e0
commit b1c87a8a9e
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
18 changed files with 526 additions and 36 deletions

View file

@ -100,7 +100,8 @@
<activity
android:name=".ui.storage.StorageActivity"
android:theme="@style/AppTheme.NoActionBar" />
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.storage.PermissionGrantActivity"

View file

@ -21,6 +21,7 @@ import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.metadataModule
import com.stevesoltys.seedvault.plugins.StoragePluginManager
import com.stevesoltys.seedvault.plugins.saf.storagePluginModuleSaf
import com.stevesoltys.seedvault.plugins.webdav.storagePluginModuleWebDav
import com.stevesoltys.seedvault.restore.RestoreViewModel
import com.stevesoltys.seedvault.restore.install.installModule
import com.stevesoltys.seedvault.settings.AppListRetriever
@ -55,7 +56,7 @@ open class App : Application() {
private val appModule = module {
single { SettingsManager(this@App) }
single { BackupNotificationManager(this@App) }
single { StoragePluginManager(this@App, get(), get()) }
single { StoragePluginManager(this@App, get(), get(), get()) }
single { Clock() }
factory<IBackupManager> { 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,

View file

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

View file

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

View file

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

View file

@ -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<StoragePlugin<*>> { WebDavStoragePlugin(androidContext(), WebDavConfig("", "", "")) }
val storagePluginModuleWebDav = module {
single { WebDavFactory(androidContext(), get()) }
single { WebDavHandler(androidContext(), get(), get(), get()) }
}

View file

@ -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<WebDavConfig>() {
override val isUsb: Boolean = false
override val requiresNetwork: Boolean = true
override fun isUnavailableUsb(context: Context): Boolean = false
}

View file

@ -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 {

View file

@ -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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<RestoreStorageViewModel>()
} else {
getSharedViewModel<BackupStorageViewModel>()
}
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
}
}
}
}

View file

@ -0,0 +1,16 @@
<!--
SPDX-FileCopyrightText: 2024 The Calyx Institute
SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorSecondary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM16.5,16L8,16c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3l0.14,0.01C8.58,8.28 10.13,7 12,7c2.21,0 4,1.79 4,4h0.5c1.38,0 2.5,1.12 2.5,2.5S17.88,16 16.5,16z" />
</vector>

View file

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?><!--
SPDX-FileCopyrightText: 2024 The Calyx Institute
SPDX-License-Identifier: Apache-2.0
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/webdavUrlLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread_inside">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/webdavUrlInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/storage_webdav_config_url"
android:inputType="text|textUri" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/webdavUserLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_constraintBottom_toTopOf="@+id/webdavPassLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/webdavUrlLayout">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/webdavUserInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/storage_webdav_config_user"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/webdavPassLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/webdavUserLayout"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/webDavPassInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/storage_webdav_config_pass"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/webdavButton"
style="@style/SudPrimaryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/storage_webdav_config_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/webdavPassLayout"
app:layout_constraintVertical_bias="1.0" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/webdavButton"
app:layout_constraintEnd_toEndOf="@+id/webdavButton"
app:layout_constraintStart_toStartOf="@+id/webdavButton"
app:layout_constraintTop_toTopOf="@+id/webdavButton"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -97,6 +97,15 @@
<string name="storage_check_fragment_permission_error">Unable to get the permission to write to the backup location.</string>
<string name="storage_check_fragment_error_button">Back</string>
<string name="storage_webdav_option_title">WebDAV Cloud (beta)</string>
<string name="storage_webdav_option_summary">Integrated direct WebDAV access</string>
<string name="storage_webdav_config_url">WebDAV URL</string>
<string name="storage_webdav_config_user">User name</string>
<string name="storage_webdav_config_pass">Password</string>
<string name="storage_webdav_config_button">Use WebDAV cloud</string>
<string name="storage_webdav_config_malformed_url">Invalid WebDAV URL</string>
<string name="storage_webdav_name">WebDAV %s</string>
<!-- Recovery Code -->
<string name="recovery_code_title">Recovery code</string>
<string name="recovery_code_12_word_intro">You need your 12-word recovery code to restore backed up data.</string>