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
|
||||
android:name=".ui.storage.StorageActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<activity
|
||||
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.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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
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()) }
|
||||
}
|
||||
|
|
|
@ -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.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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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_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>
|
||||
|
|
Loading…
Reference in a new issue