Add the UI for the WebDAV plugin
including ViewModel and StoragePluginManager logic
This commit is contained in:
parent
7e612cb8e0
commit
b1c87a8a9e
18 changed files with 526 additions and 36 deletions
|
@ -100,7 +100,8 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.storage.StorageActivity"
|
android:name=".ui.storage.StorageActivity"
|
||||||
android:theme="@style/AppTheme.NoActionBar" />
|
android:theme="@style/AppTheme.NoActionBar"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.storage.PermissionGrantActivity"
|
android:name=".ui.storage.PermissionGrantActivity"
|
||||||
|
|
|
@ -21,6 +21,7 @@ import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
import com.stevesoltys.seedvault.metadata.metadataModule
|
import com.stevesoltys.seedvault.metadata.metadataModule
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.plugins.saf.storagePluginModuleSaf
|
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.RestoreViewModel
|
||||||
import com.stevesoltys.seedvault.restore.install.installModule
|
import com.stevesoltys.seedvault.restore.install.installModule
|
||||||
import com.stevesoltys.seedvault.settings.AppListRetriever
|
import com.stevesoltys.seedvault.settings.AppListRetriever
|
||||||
|
@ -55,7 +56,7 @@ open class App : Application() {
|
||||||
private val appModule = module {
|
private val appModule = module {
|
||||||
single { SettingsManager(this@App) }
|
single { SettingsManager(this@App) }
|
||||||
single { BackupNotificationManager(this@App) }
|
single { BackupNotificationManager(this@App) }
|
||||||
single { StoragePluginManager(this@App, get(), get()) }
|
single { StoragePluginManager(this@App, get(), get(), get()) }
|
||||||
single { Clock() }
|
single { Clock() }
|
||||||
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
|
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
|
||||||
factory { AppListRetriever(this@App, get(), get(), get()) }
|
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 { 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 {
|
viewModel {
|
||||||
BackupStorageViewModel(
|
BackupStorageViewModel(
|
||||||
app = this@App,
|
app = this@App,
|
||||||
|
@ -84,11 +82,12 @@ open class App : Application() {
|
||||||
backupInitializer = get(),
|
backupInitializer = get(),
|
||||||
storageBackup = get(),
|
storageBackup = get(),
|
||||||
safHandler = get(),
|
safHandler = get(),
|
||||||
|
webDavHandler = get(),
|
||||||
settingsManager = get(),
|
settingsManager = get(),
|
||||||
storagePluginManager = 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 { RestoreViewModel(this@App, get(), get(), get(), get(), get(), get(), get()) }
|
||||||
viewModel { FileSelectionViewModel(this@App, get()) }
|
viewModel { FileSelectionViewModel(this@App, get()) }
|
||||||
}
|
}
|
||||||
|
@ -127,7 +126,8 @@ open class App : Application() {
|
||||||
cryptoModule,
|
cryptoModule,
|
||||||
headerModule,
|
headerModule,
|
||||||
metadataModule,
|
metadataModule,
|
||||||
storagePluginModuleSaf, // storage plugin
|
storagePluginModuleSaf,
|
||||||
|
storagePluginModuleWebDav,
|
||||||
backupModule,
|
backupModule,
|
||||||
restoreModule,
|
restoreModule,
|
||||||
installModule,
|
installModule,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import com.stevesoltys.seedvault.permitDiskReads
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderStoragePlugin
|
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
|
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
|
||||||
import com.stevesoltys.seedvault.plugins.saf.SafFactory
|
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.SettingsManager
|
||||||
import com.stevesoltys.seedvault.settings.StoragePluginEnum
|
import com.stevesoltys.seedvault.settings.StoragePluginEnum
|
||||||
|
|
||||||
|
@ -47,6 +48,7 @@ class StoragePluginManager(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val settingsManager: SettingsManager,
|
private val settingsManager: SettingsManager,
|
||||||
safFactory: SafFactory,
|
safFactory: SafFactory,
|
||||||
|
webDavFactory: WebDavFactory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private var _appPlugin: StoragePlugin<*>?
|
private var _appPlugin: StoragePlugin<*>?
|
||||||
|
@ -81,6 +83,14 @@ class StoragePluginManager(
|
||||||
_storageProperties = safStorage
|
_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 -> {
|
null -> {
|
||||||
_appPlugin = null
|
_appPlugin = null
|
||||||
_filesPlugin = null
|
_filesPlugin = null
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,10 +1,9 @@
|
||||||
package com.stevesoltys.seedvault.plugins.webdav
|
package com.stevesoltys.seedvault.plugins.webdav
|
||||||
|
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val webDavModule = module {
|
val storagePluginModuleWebDav = module {
|
||||||
// TODO PluginManager should create the plugin on demand
|
single { WebDavFactory(androidContext(), get()) }
|
||||||
single<StoragePlugin<*>> { WebDavStoragePlugin(androidContext(), WebDavConfig("", "", "")) }
|
single { WebDavHandler(androidContext(), get(), get(), get()) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -5,13 +5,15 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
import android.hardware.usb.UsbDevice
|
import android.hardware.usb.UsbDevice
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.annotation.UiThread
|
import androidx.annotation.UiThread
|
||||||
import androidx.annotation.WorkerThread
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.stevesoltys.seedvault.getStorageContext
|
|
||||||
import com.stevesoltys.seedvault.permitDiskReads
|
import com.stevesoltys.seedvault.permitDiskReads
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderStoragePlugin
|
import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderStoragePlugin
|
||||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
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 com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
||||||
import java.util.concurrent.ConcurrentSkipListSet
|
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
|
internal enum class StoragePluginEnum { // don't rename, will break existing installs
|
||||||
SAF,
|
SAF,
|
||||||
|
WEB_DAV,
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val PREF_KEY_STORAGE_URI = "storageUri"
|
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_VENDOR_ID = "flashDriveVendorId"
|
||||||
private const val PREF_KEY_FLASH_DRIVE_PRODUCT_ID = "flashDriveProductId"
|
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_APP_BLACKLIST = "backupAppBlacklist"
|
||||||
|
|
||||||
private const val PREF_KEY_BACKUP_STORAGE = "backup_storage"
|
private const val PREF_KEY_BACKUP_STORAGE = "backup_storage"
|
||||||
|
@ -94,7 +101,6 @@ class SettingsManager(private val context: Context) {
|
||||||
token = newToken
|
token = newToken
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME SafStorage is currently plugin specific and not generic
|
|
||||||
internal val storagePluginType: StoragePluginEnum?
|
internal val storagePluginType: StoragePluginEnum?
|
||||||
get() = prefs.getString(PREF_KEY_STORAGE_PLUGIN, StoragePluginEnum.SAF.name)?.let {
|
get() = prefs.getString(PREF_KEY_STORAGE_PLUGIN, StoragePluginEnum.SAF.name)?.let {
|
||||||
try {
|
try {
|
||||||
|
@ -107,6 +113,7 @@ class SettingsManager(private val context: Context) {
|
||||||
fun setStoragePlugin(plugin: StoragePlugin<*>) {
|
fun setStoragePlugin(plugin: StoragePlugin<*>) {
|
||||||
val value = when (plugin) {
|
val value = when (plugin) {
|
||||||
is DocumentsProviderStoragePlugin -> StoragePluginEnum.SAF
|
is DocumentsProviderStoragePlugin -> StoragePluginEnum.SAF
|
||||||
|
is WebDavStoragePlugin -> StoragePluginEnum.WEB_DAV
|
||||||
else -> error("Unsupported plugin: ${plugin::class.java.simpleName}")
|
else -> error("Unsupported plugin: ${plugin::class.java.simpleName}")
|
||||||
}.name
|
}.name
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
|
@ -159,20 +166,22 @@ class SettingsManager(private val context: Context) {
|
||||||
return FlashDrive(name, serialNumber, vendorId, productId)
|
return FlashDrive(name, serialNumber, vendorId, productId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
val webDavProperties: WebDavProperties?
|
||||||
* Check if we are able to do backups now by examining possible pre-conditions
|
get() {
|
||||||
* such as plugged-in flash drive or internet access.
|
val config = WebDavConfig(
|
||||||
*
|
url = prefs.getString(PREF_KEY_WEBDAV_URL, null) ?: return null,
|
||||||
* Should be run off the UI thread (ideally I/O) because of disk access.
|
username = prefs.getString(PREF_KEY_WEBDAV_USER, null) ?: return null,
|
||||||
*
|
password = prefs.getString(PREF_KEY_WEBDAV_PASS, null) ?: return null,
|
||||||
* @return true if a backup is possible, false if not.
|
)
|
||||||
*/
|
return createWebDavProperties(context, config)
|
||||||
@WorkerThread
|
}
|
||||||
fun canDoBackupNow(): Boolean {
|
|
||||||
val storage = getSafStorage() ?: return false
|
fun saveWebDavConfig(config: WebDavConfig) {
|
||||||
val systemContext = context.getStorageContext { storage.isUsb }
|
prefs.edit()
|
||||||
return !storage.isUnavailableUsb(systemContext) &&
|
.putString(PREF_KEY_WEBDAV_URL, config.url)
|
||||||
!storage.isUnavailableNetwork(context, useMeteredNetwork)
|
.putString(PREF_KEY_WEBDAV_USER, config.username)
|
||||||
|
.putString(PREF_KEY_WEBDAV_PASS, config.password)
|
||||||
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun backupApks(): Boolean {
|
fun backupApks(): Boolean {
|
||||||
|
|
|
@ -10,6 +10,9 @@ import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.plugins.saf.SafHandler
|
import com.stevesoltys.seedvault.plugins.saf.SafHandler
|
||||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
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.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.storage.StorageBackupJobService
|
import com.stevesoltys.seedvault.storage.StorageBackupJobService
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupInitializer
|
import com.stevesoltys.seedvault.transport.backup.BackupInitializer
|
||||||
|
@ -29,9 +32,10 @@ internal class BackupStorageViewModel(
|
||||||
private val backupInitializer: BackupInitializer,
|
private val backupInitializer: BackupInitializer,
|
||||||
private val storageBackup: StorageBackup,
|
private val storageBackup: StorageBackup,
|
||||||
safHandler: SafHandler,
|
safHandler: SafHandler,
|
||||||
|
webDavHandler: WebDavHandler,
|
||||||
settingsManager: SettingsManager,
|
settingsManager: SettingsManager,
|
||||||
storagePluginManager: StoragePluginManager,
|
storagePluginManager: StoragePluginManager,
|
||||||
) : StorageViewModel(app, safHandler, settingsManager, storagePluginManager) {
|
) : StorageViewModel(app, safHandler, webDavHandler, settingsManager, storagePluginManager) {
|
||||||
|
|
||||||
override val isRestoreOperation = false
|
override val isRestoreOperation = false
|
||||||
|
|
||||||
|
@ -49,6 +53,13 @@ internal class BackupStorageViewModel(
|
||||||
onStorageLocationSet(safStorage.isUsb)
|
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) {
|
private fun onStorageLocationSet(isUsb: Boolean) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -8,6 +8,9 @@ import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DIRECTORY_ROOT
|
import com.stevesoltys.seedvault.plugins.saf.DIRECTORY_ROOT
|
||||||
import com.stevesoltys.seedvault.plugins.saf.SafHandler
|
import com.stevesoltys.seedvault.plugins.saf.SafHandler
|
||||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
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.settings.SettingsManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -18,9 +21,10 @@ private val TAG = RestoreStorageViewModel::class.java.simpleName
|
||||||
internal class RestoreStorageViewModel(
|
internal class RestoreStorageViewModel(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
safHandler: SafHandler,
|
safHandler: SafHandler,
|
||||||
|
webDavHandler: WebDavHandler,
|
||||||
settingsManager: SettingsManager,
|
settingsManager: SettingsManager,
|
||||||
storagePluginManager: StoragePluginManager,
|
storagePluginManager: StoragePluginManager,
|
||||||
) : StorageViewModel(app, safHandler, settingsManager, storagePluginManager) {
|
) : StorageViewModel(app, safHandler, webDavHandler, settingsManager, storagePluginManager) {
|
||||||
|
|
||||||
override val isRestoreOperation = true
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package com.stevesoltys.seedvault.ui.storage
|
package com.stevesoltys.seedvault.ui.storage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.DocumentsContract.buildTreeDocumentUri
|
import android.provider.DocumentsContract.buildTreeDocumentUri
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources.getDrawable
|
||||||
|
import com.stevesoltys.seedvault.R
|
||||||
|
|
||||||
internal sealed class StorageOption {
|
internal sealed class StorageOption {
|
||||||
abstract val id: String
|
abstract val id: String
|
||||||
|
@ -46,3 +49,14 @@ internal sealed class StorageOption {
|
||||||
return id.hashCode()
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -62,7 +62,9 @@ internal class StorageOptionFetcher(private val context: Context, private val is
|
||||||
internal fun getRemovableStorageListener() = listener
|
internal fun getRemovableStorageListener() = listener
|
||||||
|
|
||||||
internal fun getStorageOptions(): List<StorageOption> {
|
internal fun getStorageOptions(): List<StorageOption> {
|
||||||
val roots = ArrayList<StorageOption>()
|
val roots = ArrayList<StorageOption>().apply {
|
||||||
|
add(WebDavOption(context))
|
||||||
|
}
|
||||||
val intent = Intent(PROVIDER_INTERFACE)
|
val intent = Intent(PROVIDER_INTERFACE)
|
||||||
val providers = packageManager.queryIntentContentProviders(intent, 0)
|
val providers = packageManager.queryIntentContentProviders(intent, 0)
|
||||||
for (info in providers) {
|
for (info in providers) {
|
||||||
|
|
|
@ -115,11 +115,21 @@ internal class StorageOptionsFragment : Fragment(), StorageOptionClickedListener
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(storageOption: StorageOption) {
|
override fun onClick(storageOption: StorageOption) {
|
||||||
if (storageOption is SafOption) {
|
when (storageOption) {
|
||||||
viewModel.onSafOptionChosen(storageOption)
|
is SafOption -> {
|
||||||
openDocumentTree.launch(storageOption.uri)
|
viewModel.onSafOptionChosen(storageOption)
|
||||||
} else {
|
openDocumentTree.launch(storageOption.uri)
|
||||||
throw IllegalArgumentException("Non-SAF storage not yet supported")
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,10 @@ import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import com.stevesoltys.seedvault.plugins.saf.SafHandler
|
import com.stevesoltys.seedvault.plugins.saf.SafHandler
|
||||||
import com.stevesoltys.seedvault.plugins.saf.SafStorage
|
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.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.ui.LiveEvent
|
import com.stevesoltys.seedvault.ui.LiveEvent
|
||||||
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
||||||
|
@ -21,6 +25,7 @@ import kotlinx.coroutines.launch
|
||||||
internal abstract class StorageViewModel(
|
internal abstract class StorageViewModel(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
protected val safHandler: SafHandler,
|
protected val safHandler: SafHandler,
|
||||||
|
protected val webdavHandler: WebDavHandler,
|
||||||
protected val settingsManager: SettingsManager,
|
protected val settingsManager: SettingsManager,
|
||||||
protected val storagePluginManager: StoragePluginManager,
|
protected val storagePluginManager: StoragePluginManager,
|
||||||
) : AndroidViewModel(app), RemovableStorageListener {
|
) : AndroidViewModel(app), RemovableStorageListener {
|
||||||
|
@ -79,11 +84,26 @@ internal abstract class StorageViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun onSafUriSet(safStorage: SafStorage)
|
abstract fun onSafUriSet(safStorage: SafStorage)
|
||||||
|
abstract fun onWebDavConfigSet(properties: WebDavProperties, plugin: WebDavStoragePlugin)
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
storageOptionFetcher.setRemovableStorageListener(null)
|
storageOptionFetcher.setRemovableStorageListener(null)
|
||||||
super.onCleared()
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
16
app/src/main/res/drawable/ic_cloud_circle.xml
Normal file
16
app/src/main/res/drawable/ic_cloud_circle.xml
Normal 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>
|
89
app/src/main/res/layout/fragment_webdav_config.xml
Normal file
89
app/src/main/res/layout/fragment_webdav_config.xml
Normal 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>
|
|
@ -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_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_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 -->
|
<!-- Recovery Code -->
|
||||||
<string name="recovery_code_title">Recovery code</string>
|
<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>
|
<string name="recovery_code_12_word_intro">You need your 12-word recovery code to restore backed up data.</string>
|
||||||
|
|
Loading…
Reference in a new issue