refactor(errors): унифицировал доменные ошибки и добавил failed-статус задач

This commit is contained in:
2026-05-18 14:52:33 +03:00
parent a1226a8803
commit f3f99aed5a
38 changed files with 498 additions and 112 deletions

View File

@@ -6,6 +6,7 @@ import androidx.activity.result.ActivityResultLauncher
import com.github.nullptroma.wallenc.domain.vault.vaults.yandex.YandexRegistration
import com.github.nullptroma.wallenc.vault.contract.CloudBrand
import com.github.nullptroma.wallenc.vault.contract.RemoteVaultAuthenticator
import com.github.nullptroma.wallenc.vault.contract.VaultLinkFailure
import com.github.nullptroma.wallenc.vault.contract.VaultLinkOutcome
import com.yandex.authsdk.YandexAuthLoginOptions
import com.yandex.authsdk.YandexAuthOptions
@@ -70,16 +71,12 @@ class YandexSignInService @Inject constructor(
override fun beginLink(brand: CloudBrand, onResult: (VaultLinkOutcome) -> Unit) {
if (brand != CloudBrand.YANDEX) {
onResult(VaultLinkOutcome.Failed("Brand $brand is not supported by YandexSignInService"))
onResult(VaultLinkOutcome.Failed(VaultLinkFailure.UnsupportedBrand))
return
}
val l = launcher
if (l == null) {
onResult(
VaultLinkOutcome.Failed(
"YandexSignInService: call registerWith(activity) from MainActivity.onCreate first",
),
)
onResult(VaultLinkOutcome.Failed(VaultLinkFailure.NotRegistered))
return
}
synchronized(this) { pending = onResult }
@@ -90,9 +87,7 @@ class YandexSignInService @Inject constructor(
is YandexAuthResult.Success ->
VaultLinkOutcome.Success(YandexRegistration(result.token.value))
is YandexAuthResult.Failure ->
VaultLinkOutcome.Failed(
result.exception.message ?: result.exception.toString(),
)
VaultLinkOutcome.Failed(VaultLinkFailure.AuthError)
YandexAuthResult.Cancelled -> VaultLinkOutcome.Cancelled
}
}

View File

@@ -0,0 +1,54 @@
package com.github.nullptroma.wallenc.domain.vault.errors
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.domain.errors.toWallencException
import com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.YandexDiskAuthException
import retrofit2.HttpException
import java.io.FileNotFoundException
import java.io.IOException
fun Throwable.toVaultWallencException(): WallencException = when (this) {
is WallencException -> this
is YandexDiskAuthException -> WallencException.Auth.Failed
is HttpException -> WallencException.Network.HttpFailed(
operation = "http",
statusCode = code(),
cause = this,
)
is FileNotFoundException -> WallencException.Storage.FileNotFound
is IOException -> mapVaultIo(this)
is IllegalStateException -> mapIllegalState(this)
is IllegalArgumentException -> mapIllegalArgument(this)
else -> toWallencException()
}
private fun mapVaultIo(e: IOException): WallencException {
val msg = e.message.orEmpty()
return when {
msg.contains("OAuth token is missing", ignoreCase = true) ->
WallencException.Auth.TokenMissing
msg.contains("HTTP 423", ignoreCase = true) || msg.contains("423 after retries", ignoreCase = true) ->
WallencException.Network.ResourceLocked
msg.contains("async operation timed out", ignoreCase = true) ->
WallencException.Network.OperationTimedOut
msg.contains("async operation failed", ignoreCase = true) ->
WallencException.Network.OperationFailed
else -> WallencException.Network.IoFailed(e)
}
}
private fun mapIllegalState(e: IllegalStateException): WallencException = when {
e.message == "Not a file" -> WallencException.Storage.NotAFile
e.message == "Not a directory" -> WallencException.Storage.NotADirectory
e.message?.startsWith("Path segment is a file:") == true -> WallencException.Storage.PathIsFile
e.message?.startsWith("Cannot openWrite over directory:") == true ->
WallencException.Storage.CannotWriteOverDirectory
e.message?.startsWith("Expected file after upload:") == true ->
WallencException.Storage.UnexpectedState
else -> WallencException.Storage.UnexpectedState
}
private fun mapIllegalArgument(e: IllegalArgumentException): WallencException = when {
e.message == "Deleting root path is forbidden" -> WallencException.Storage.DeleteRootForbidden
else -> WallencException.Unknown(e)
}

View File

@@ -1,5 +1,6 @@
package com.github.nullptroma.wallenc.domain.vault.storages
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.domain.vault.model.StorageKeyMap
import com.github.nullptroma.wallenc.domain.vault.ports.StorageKeyMapStore
import com.github.nullptroma.wallenc.domain.vault.storages.encrypt.EncryptedStorage
@@ -109,9 +110,9 @@ class UnlockManager(
rememberPassword: Boolean
): EncryptedStorage = withContext(ioDispatcher) {
return@withContext mutex.withLock {
val encInfo = storage.metaInfo.value.encInfo ?: throw Exception("EncInfo is null") // TODO
val encInfo = storage.metaInfo.value.encInfo ?: throw WallencException.Storage.EncInfoMissing
if (!Encryptor.checkKey(key, encInfo))
throw Exception("Incorrect Key")
throw WallencException.Storage.IncorrectKey
val opened = _openedStorages.value.toMutableMap()
val cur = opened[storage.uuid]

View File

@@ -1,5 +1,7 @@
package com.github.nullptroma.wallenc.domain.vault.storages.encrypt
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.domain.vault.storages.common.BaseStorage
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
import com.github.nullptroma.wallenc.domain.encrypt.Encryptor
@@ -27,7 +29,7 @@ class EncryptedStorage private constructor(
private val scope = CoroutineScope(ioDispatcher + job)
private val encInfo =
source.metaInfo.value.encInfo ?: throw Exception("Storage is not encrypted") // TODO
source.metaInfo.value.encInfo ?: throw WallencException.Storage.NotEncrypted
override val isVirtualStorage: Boolean = true
@@ -50,7 +52,7 @@ class EncryptedStorage private constructor(
private fun checkKey() {
if (!Encryptor.checkKey(key, encInfo))
throw Exception("Incorrect key") // TODO
throw WallencException.Storage.IncorrectKey
}
fun getKey(): EncryptKey = EncryptKey(key.bytes)

View File

@@ -1,5 +1,7 @@
package com.github.nullptroma.wallenc.domain.vault.storages.local
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.fasterxml.jackson.core.JacksonException
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
@@ -230,7 +232,7 @@ class LocalStorageAccessor(
dirCallback: (suspend (File, CommonDirectory) -> Unit)? = null
) {
if (!checkAvailable())
throw Exception("Not available")
throw WallencException.Storage.NotAvailable
val basePath = Path(_filesystemBasePath.pathString, baseStoragePath)
val workedFiles = mutableSetOf<String>()
val workedMetaFiles = mutableSetOf<String>()
@@ -396,7 +398,7 @@ class LocalStorageAccessor(
override suspend fun getFileInfo(path: String): IFile {
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
?: throw Exception("Что то пошло не так") // TODO
?: throw WallencException.Storage.UnexpectedState
return CommonFile(
metaInfo = pair.meta,
)
@@ -404,7 +406,7 @@ class LocalStorageAccessor(
override suspend fun getDirInfo(path: String): IDirectory {
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
?: throw Exception("Что то пошло не так") // TODO
?: throw WallencException.Storage.UnexpectedState
return CommonDirectory(
metaInfo = pair.meta,
elementsCount = null
@@ -413,7 +415,7 @@ class LocalStorageAccessor(
override suspend fun setHidden(path: String, hidden: Boolean) {
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
?: throw Exception("Что то пошло не так") // TODO
?: throw WallencException.Storage.UnexpectedState
if (pair.meta.isHidden == hidden)
return
val newMeta = pair.meta.copy(isHidden = hidden)
@@ -440,7 +442,7 @@ class LocalStorageAccessor(
val path = Path(_filesystemBasePath.pathString, storagePath)
val file = path.toFile()
if (file.exists() && file.isDirectory) {
throw Exception("Что то пошло не так") // TODO
throw WallencException.Storage.UnexpectedState
} else if(!file.exists()) {
val parent = Path(storagePath).parent
createDir(parent.pathString)
@@ -451,7 +453,7 @@ class LocalStorageAccessor(
}
val pair = LocalStorageFilePair.from(_filesystemBasePath, file)
?: throw Exception("Что то пошло не так") // TODO
?: throw WallencException.Storage.UnexpectedState
val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant(), size = Files.size(pair.file.toPath()))
writeMeta(pair.metaFile, newMeta)
_filesUpdates.emit(
@@ -468,13 +470,13 @@ class LocalStorageAccessor(
val path = Path(_filesystemBasePath.pathString, storagePath)
val file = path.toFile()
if (file.exists() && !file.isDirectory) {
throw Exception("Что то пошло не так") // TODO
throw WallencException.Storage.UnexpectedState
} else if(!file.exists()) {
Files.createDirectories(path)
}
val pair = LocalStorageFilePair.from(_filesystemBasePath, file)
?: throw Exception("Что то пошло не так") // TODO
?: throw WallencException.Storage.UnexpectedState
val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant())
writeMeta(pair.metaFile, newMeta)
_dirsUpdates.emit(
@@ -512,7 +514,7 @@ class LocalStorageAccessor(
override suspend fun delete(path: String) = withContext(ioDispatcher) {
if (path == "/" || path.isBlank()) {
throw IllegalArgumentException("Deleting root path is forbidden")
throw WallencException.Storage.DeleteRootForbidden
}
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
if (pair != null) {
@@ -527,7 +529,7 @@ class LocalStorageAccessor(
override suspend fun openWrite(path: String): OutputStream = withContext(ioDispatcher) {
touchFileInternal(path, recordJournal = false)
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
?: throw Exception("Файла нет") // TODO
?: throw WallencException.Storage.FileNotFound
return@withContext pair.file.outputStream().onClosed {
CoroutineScope(ioDispatcher).launch {
touchFileInternal(path, recordJournal = false)
@@ -539,13 +541,13 @@ class LocalStorageAccessor(
override suspend fun openRead(path: String): InputStream = withContext(ioDispatcher) {
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
?: throw Exception("Файла нет") // TODO
?: throw WallencException.Storage.FileNotFound
return@withContext pair.file.inputStream()
}
override suspend fun moveToTrash(path: String) = withContext(ioDispatcher) {
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
?: throw Exception("Файла нет") // TODO
?: throw WallencException.Storage.FileNotFound
val newMeta = pair.meta.copy(isDeleted = true)
writeMeta(pair.metaFile, newMeta)
appendSyncEntry(path = path, operation = StorageSyncOperation.DELETE)

View File

@@ -1,5 +1,8 @@
package com.github.nullptroma.wallenc.domain.vault.storages.yandex
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.domain.vault.errors.toVaultWallencException
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.YandexDiskAuthException
import com.github.nullptroma.wallenc.domain.vault.network.yandexdisk.dto.ResourceDto
@@ -104,19 +107,23 @@ class YandexStorageAccessor(
} catch (e: YandexDiskAuthException) {
reportAuthFailure()
_storageReady.value = false
throw e
throw WallencException.Auth.Failed
} catch (e: Exception) {
_storageReady.value = false
throw Exception("Yandex storage init failed", e)
throw e.toVaultWallencException()
}
}
private inline fun <T> guard(block: () -> T): T {
try {
return block()
} catch (e: CancellationException) {
throw e
} catch (e: YandexDiskAuthException) {
reportAuthFailure()
throw e
throw WallencException.Auth.Failed
} catch (e: Throwable) {
throw e.toVaultWallencException()
}
}
@@ -445,13 +452,13 @@ class YandexStorageAccessor(
override suspend fun getFileInfo(path: String): IFile = withContext(ioDispatcher) {
val r = guard { repo.get(toDiskPath(path)) }
if (r.type != "file") throw IllegalStateException("Not a file")
if (r.type != "file") throw WallencException.Storage.NotAFile
r.toCommonFile(path)
}
override suspend fun getDirInfo(path: String): IDirectory = withContext(ioDispatcher) {
val r = guard { repo.get(toDiskPath(path)) }
if (r.type != "dir") throw IllegalStateException("Not a directory")
if (r.type != "dir") throw WallencException.Storage.NotADirectory
r.toCommonDir(path)
}
@@ -490,7 +497,7 @@ class YandexStorageAccessor(
val diskPath = toDiskPath(acc)
when (guard { repo.getOrNull(diskPath) }?.type) {
"dir" -> continue
"file" -> throw IllegalStateException("Path segment is a file: $acc")
"file" -> throw WallencException.Storage.PathIsFile
else -> guard { repo.createFolder(diskPath) }
}
}
@@ -498,7 +505,7 @@ class YandexStorageAccessor(
override suspend fun delete(path: String) = withContext(ioDispatcher) {
if (path == "/" || path.isBlank()) {
throw IllegalArgumentException("Deleting root path is forbidden")
throw WallencException.Storage.DeleteRootForbidden
}
val diskPath = toDiskPath(path)
val prior = guard { repo.getOrNull(diskPath) }
@@ -531,14 +538,14 @@ class YandexStorageAccessor(
val diskPath = toDiskPath(path)
val prior = guard { repo.getOrNull(diskPath) }
if (prior?.type == "dir") {
throw IllegalStateException("Cannot openWrite over directory: $path")
throw WallencException.Storage.CannotWriteOverDirectory
}
val hadFile = prior?.type == "file"
val priorSize = if (prior?.type == "file") prior.size ?: 0L else 0L
guard { repo.uploadFile(diskPath, tmp, overwrite = true) }
val after = guard { getMetadataAfterWrite(diskPath) }
if (after.type != "file") {
throw IllegalStateException("Expected file after upload: $path")
throw WallencException.Storage.UnexpectedState
}
val newSize = after.size ?: 0L
_size.value = ((_size.value ?: 0L) + newSize - priorSize).coerceAtLeast(0L)

View File

@@ -1,5 +1,7 @@
package com.github.nullptroma.wallenc.domain.vault.vaults.local
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.vault.storages.local.LocalStorage
@@ -64,7 +66,7 @@ class LocalVault(
private suspend fun readStorages() {
val path = path.value
if (path == null || !_isAvailable.value) {
throw Exception("Not available")
throw WallencException.Storage.NotAvailable
}
val dirs = path.listFiles()?.filter { it.isDirectory }
@@ -79,7 +81,7 @@ class LocalVault(
override suspend fun createStorage(): LocalStorage = withContext(ioDispatcher) {
val path = path.value
if (path == null || !_isAvailable.value) {
throw Exception("Not available")
throw WallencException.Storage.NotAvailable
}
val storageUuid = UUID.randomUUID()
@@ -104,7 +106,7 @@ class LocalVault(
override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) {
val path = path.value
if (path == null || !_isAvailable.value) {
throw Exception("Not available")
throw WallencException.Storage.NotAvailable
}
val curStorages = _storages.value.toMutableList()

View File

@@ -0,0 +1,55 @@
package com.github.nullptroma.wallenc.domain.errors
/**
* Единая иерархия сбоев Wallenc. Ожидаемые бизнес-отказы (CanEncryptResult и т.п.)
* остаются отдельными sealed-типами; сюда попадают только операционные ошибки.
*/
sealed class WallencException(
message: String? = null,
cause: Throwable? = null,
) : Exception(message, cause) {
sealed class Feature : WallencException() {
data object StorageNotFound : Feature()
data object NeedsDecryptedView : Feature()
data object SecretNotFound : Feature()
data object StorageNotWritable : Feature()
}
sealed class Storage(cause: Throwable? = null) : WallencException(cause = cause) {
data object NotAvailable : Storage()
data object FileNotFound : Storage()
data object IncorrectKey : Storage()
data class IoFailed(override val cause: Throwable) : Storage(cause)
data object EncInfoMissing : Storage()
data object NotEncrypted : Storage()
data object NotWritable : Storage()
data object NotAFile : Storage()
data object NotADirectory : Storage()
data object PathIsFile : Storage()
data object CannotWriteOverDirectory : Storage()
data object DeleteRootForbidden : Storage()
data object UnexpectedState : Storage()
}
/** Ошибки аутентификации (OAuth, токен), без привязки к провайдеру. */
sealed class Auth : WallencException() {
data object Failed : Auth()
data object TokenMissing : Auth()
}
/** Сетевые и удалённые операции (HTTP, блокировки, таймауты). */
sealed class Network(cause: Throwable? = null) : WallencException(cause = cause) {
data class HttpFailed(
val operation: String,
val statusCode: Int,
override val cause: Throwable? = null,
) : Network(cause)
data class IoFailed(override val cause: Throwable) : Network(cause)
data object ResourceLocked : Network()
data object OperationFailed : Network()
data object OperationTimedOut : Network()
}
data class Unknown(override val cause: Throwable?) : WallencException(cause = cause)
}

View File

@@ -0,0 +1,13 @@
package com.github.nullptroma.wallenc.domain.errors
import java.io.FileNotFoundException
import java.io.IOException
fun Throwable.toWallencException(): WallencException = when (this) {
is WallencException -> this
is FileNotFoundException -> WallencException.Storage.FileNotFound
is IOException -> WallencException.Network.IoFailed(this)
else -> WallencException.Unknown(this)
}
fun Throwable.rethrowAsWallencException(): Nothing = throw toWallencException()

View File

@@ -1,5 +1,7 @@
package com.github.nullptroma.wallenc.domain.tasks
import com.github.nullptroma.wallenc.domain.errors.WallencException
interface TaskContext {
val taskId: TaskId
@@ -8,4 +10,6 @@ interface TaskContext {
suspend fun reportProgress(progress: TaskProgress) = reportProgress(progress.fraction, progress.label)
fun log(level: TaskLogLevel, message: String)
fun fail(error: WallencException): Nothing
}

View File

@@ -1,9 +1,11 @@
package com.github.nullptroma.wallenc.domain.tasks
import com.github.nullptroma.wallenc.domain.errors.WallencException
sealed class TaskRunState {
data object Queued : TaskRunState()
data class Running(val progress: TaskProgress?) : TaskRunState()
data object Completed : TaskRunState()
data object Cancelled : TaskRunState()
data class Failed(val message: String) : TaskRunState()
data class Failed(val error: WallencException) : TaskRunState()
}

View File

@@ -0,0 +1,39 @@
package com.github.nullptroma.wallenc.domain.errors
import org.junit.Assert.assertEquals
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.FileNotFoundException
import java.io.IOException
class WallencExceptionMappingTest {
@Test
fun preservesWallencException() {
val original = WallencException.Feature.StorageNotFound
assertSame(original, original.toWallencException())
}
@Test
fun mapsFileNotFoundException() {
val mapped = FileNotFoundException("missing").toWallencException()
assertEquals(WallencException.Storage.FileNotFound, mapped)
}
@Test
fun mapsIOExceptionToIoFailed() {
val cause = IOException("disk")
val mapped = cause.toWallencException()
assertTrue(mapped is WallencException.Network.IoFailed)
assertSame(cause, (mapped as WallencException.Network.IoFailed).cause)
}
@Test
fun mapsGenericExceptionToUnknown() {
val cause = Exception("boom")
val mapped = cause.toWallencException()
assertTrue(mapped is WallencException.Unknown)
assertSame(cause, (mapped as WallencException.Unknown).cause)
}
}

View File

@@ -1,5 +1,7 @@
package com.github.nullptroma.wallenc.task.runtime
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.domain.errors.toWallencException
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
import com.github.nullptroma.wallenc.domain.tasks.PipelineState
import com.github.nullptroma.wallenc.domain.tasks.PipelineTask
@@ -188,9 +190,13 @@ class TaskOrchestrator(
replaceTask(taskId) { it.copy(state = TaskRunState.Completed) }
} catch (_: CancellationException) {
replaceTask(taskId) { it.copy(state = TaskRunState.Cancelled) }
} catch (e: TaskFailedException) {
replaceTask(taskId) {
it.copy(state = TaskRunState.Failed(e.error))
}
} catch (e: Exception) {
replaceTask(taskId) {
it.copy(state = TaskRunState.Failed(e.message ?: e.toString()))
it.copy(state = TaskRunState.Failed(e.toWallencException()))
}
} finally {
cancelRequested.remove(taskId)
@@ -216,7 +222,13 @@ class TaskOrchestrator(
override fun log(level: TaskLogLevel, message: String) {
appendLog(level, message)
}
override fun fail(error: WallencException): Nothing {
throw TaskFailedException(error)
}
}
private class TaskFailedException(val error: WallencException) : RuntimeException()
companion object {
private const val MAX_LOG_LINES = 500

View File

@@ -0,0 +1,86 @@
package com.github.nullptroma.wallenc.ui.resources
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.usecases.AddStorageToSyncGroupResult
import com.github.nullptroma.wallenc.vault.contract.VaultLinkFailure
fun WallencException.toUserNotification(): UserNotification.TextRes = when (this) {
WallencException.Feature.StorageNotFound ->
UserNotification.TextRes(R.string.error_storage_not_found)
WallencException.Feature.NeedsDecryptedView ->
UserNotification.TextRes(R.string.error_storage_locked_view)
WallencException.Feature.SecretNotFound ->
UserNotification.TextRes(R.string.error_secret_not_found)
WallencException.Feature.StorageNotWritable ->
UserNotification.TextRes(R.string.error_storage_not_writable)
WallencException.Storage.NotAvailable,
WallencException.Storage.NotWritable,
->
UserNotification.TextRes(R.string.error_storage_not_writable)
WallencException.Storage.FileNotFound ->
UserNotification.TextRes(R.string.error_file_not_found)
WallencException.Storage.IncorrectKey ->
UserNotification.TextRes(R.string.error_incorrect_password)
WallencException.Storage.NotEncrypted ->
UserNotification.TextRes(R.string.error_storage_not_encrypted)
WallencException.Storage.EncInfoMissing ->
UserNotification.TextRes(R.string.error_enc_info_missing)
WallencException.Storage.DeleteRootForbidden ->
UserNotification.TextRes(R.string.error_delete_root_forbidden)
WallencException.Storage.NotAFile ->
UserNotification.TextRes(R.string.error_not_a_file)
WallencException.Storage.NotADirectory ->
UserNotification.TextRes(R.string.error_not_a_directory)
WallencException.Storage.PathIsFile ->
UserNotification.TextRes(R.string.error_path_is_file)
WallencException.Storage.CannotWriteOverDirectory ->
UserNotification.TextRes(R.string.error_cannot_write_over_directory)
WallencException.Storage.UnexpectedState ->
UserNotification.TextRes(R.string.error_unexpected_state)
is WallencException.Storage.IoFailed ->
UserNotification.TextRes(R.string.error_network)
WallencException.Auth.Failed,
WallencException.Auth.TokenMissing,
->
UserNotification.TextRes(R.string.vault_link_error_auth)
WallencException.Network.ResourceLocked ->
UserNotification.TextRes(R.string.error_disk_resource_locked)
WallencException.Network.OperationFailed,
WallencException.Network.OperationTimedOut,
is WallencException.Network.HttpFailed,
is WallencException.Network.IoFailed,
->
UserNotification.TextRes(R.string.error_network)
is WallencException.Unknown ->
UserNotification.TextRes(R.string.error_unknown)
}
fun VaultLinkFailure.toUserNotification(): UserNotification.TextRes = when (this) {
VaultLinkFailure.UnsupportedBrand ->
UserNotification.TextRes(R.string.vault_link_error_unsupported_brand)
VaultLinkFailure.NotRegistered ->
UserNotification.TextRes(R.string.vault_link_error_not_registered)
VaultLinkFailure.AuthError ->
UserNotification.TextRes(R.string.vault_link_error_auth)
VaultLinkFailure.Unknown ->
UserNotification.TextRes(R.string.vault_link_error_unknown)
}
fun AddStorageToSyncGroupResult.toUserNotification(groupId: String): UserNotification.TextRes = when (this) {
AddStorageToSyncGroupResult.Added ->
UserNotification.TextRes(R.string.sync_msg_storage_added, listOf(groupId))
AddStorageToSyncGroupResult.GroupNotFound ->
UserNotification.TextRes(R.string.sync_error_group_not_found)
AddStorageToSyncGroupResult.AlreadyInGroup ->
UserNotification.TextRes(R.string.sync_msg_storage_already_added)
AddStorageToSyncGroupResult.EncryptedStorageNotAllowed ->
UserNotification.TextRes(R.string.sync_msg_only_plain_storage_allowed)
AddStorageToSyncGroupResult.MissingEncryptionSecret ->
UserNotification.TextRes(R.string.sync_msg_storage_encryption_key_required)
AddStorageToSyncGroupResult.IncompatibleEncryption ->
UserNotification.TextRes(R.string.sync_msg_storage_incompatible_encryption)
}

View File

@@ -0,0 +1,16 @@
package com.github.nullptroma.wallenc.ui.resources
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
@Composable
fun UserNotification.resolveText(): String = when (this) {
is UserNotification.TextRes -> {
if (formatArgs.isEmpty()) {
stringResource(id)
} else {
stringResource(id, *formatArgs.toTypedArray())
}
}
is UserNotification.Plain -> message
}

View File

@@ -46,6 +46,7 @@ import androidx.compose.ui.window.Dialog
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
import com.github.nullptroma.wallenc.vault.contract.CloudBrand
import com.github.nullptroma.wallenc.vault.contract.VaultLinkOutcome
@@ -211,9 +212,18 @@ fun RemoteVaultsScreen(
when (outcome) {
is VaultLinkOutcome.Success ->
viewModel.onLinkSucceeded(outcome.registration)
is VaultLinkOutcome.Failed ->
Toast.makeText(context, outcome.message, Toast.LENGTH_LONG)
.show()
is VaultLinkOutcome.Failed -> {
val notification = outcome.reason.toUserNotification()
val text = if (notification.formatArgs.isEmpty()) {
context.getString(notification.id)
} else {
context.getString(
notification.id,
*notification.formatArgs.toTypedArray(),
)
}
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
}
VaultLinkOutcome.Cancelled -> { }
}
}

View File

@@ -27,6 +27,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.resources.resolveText
@Composable
fun StorageHomeScreen(
@@ -80,9 +81,9 @@ fun StorageHomeScreen(
),
)
uiState.errorMessage?.let {
uiState.errorNotification?.let { notification ->
Text(
text = it,
text = notification.resolveText(),
color = MaterialTheme.colorScheme.error,
)
}

View File

@@ -1,6 +1,7 @@
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage
import androidx.compose.runtime.Immutable
import com.github.nullptroma.wallenc.ui.resources.UserNotification
@Immutable
data class StorageHomeScreenState(
@@ -13,5 +14,5 @@ data class StorageHomeScreenState(
val twoFaCount: Int = 0,
val textSecretsCount: Int = 0,
val canManageDomainData: Boolean = false,
val errorMessage: String? = null,
val errorNotification: UserNotification.TextRes? = null,
)

View File

@@ -2,7 +2,9 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.storage
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.ui.ViewModelBase
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
import com.github.nullptroma.wallenc.usecases.ManageTextSecretsUseCase
import com.github.nullptroma.wallenc.usecases.ManageTwoFaTokensUseCase
@@ -33,7 +35,7 @@ class StorageHomeViewModel @Inject constructor(
state.value.copy(
isLoading = false,
storageUuid = storageUuid.toString(),
errorMessage = "Storage not found",
errorNotification = WallencException.Feature.StorageNotFound.toUserNotification(),
),
)
return@launch
@@ -56,8 +58,8 @@ class StorageHomeViewModel @Inject constructor(
twoFaCount = twoFa.size,
textSecretsCount = secrets.size,
canManageDomainData = canManageDomainData,
errorMessage = if (isRawEncrypted) {
"Откройте расшифрованное отображение storage для работы с 2FA и секретами"
errorNotification = if (isRawEncrypted) {
WallencException.Feature.NeedsDecryptedView.toUserNotification()
} else {
null
},

View File

@@ -33,6 +33,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.resources.resolveText
import kotlinx.coroutines.launch
@Composable
@@ -58,8 +59,11 @@ fun TextSecretDetailsScreen(
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
uiState.errorMessage?.let {
Text(it, color = MaterialTheme.colorScheme.error)
uiState.errorNotification?.let { notification ->
Text(
text = notification.resolveText(),
color = MaterialTheme.colorScheme.error,
)
}
if (uiState.isMutating) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())

View File

@@ -1,6 +1,7 @@
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.secrets
import androidx.compose.runtime.Immutable
import com.github.nullptroma.wallenc.ui.resources.UserNotification
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
@Immutable
@@ -9,5 +10,5 @@ data class TextSecretDetailsScreenState(
val isAvailable: Boolean = false,
val isMutating: Boolean = false,
val secret: TextSecretRecord? = null,
val errorMessage: String? = null,
val errorNotification: UserNotification.TextRes? = null,
)

View File

@@ -6,7 +6,9 @@ import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
import com.github.nullptroma.wallenc.domain.tasks.TaskId
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.ui.ViewModelBase
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.requireStorageUuid
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
@@ -44,7 +46,7 @@ class TextSecretDetailsViewModel @Inject constructor(
updateState(
state.value.copy(
isLoading = false,
errorMessage = "Storage not found",
errorNotification = WallencException.Feature.StorageNotFound.toUserNotification(),
),
)
return@launch
@@ -54,7 +56,7 @@ class TextSecretDetailsViewModel @Inject constructor(
state.value.copy(
isLoading = false,
isAvailable = false,
errorMessage = "Откройте расшифрованное отображение storage для просмотра и редактирования секрета",
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
),
)
return@launch
@@ -75,7 +77,11 @@ class TextSecretDetailsViewModel @Inject constructor(
isAvailable = available,
isMutating = isMutating,
secret = secret,
errorMessage = if (secret == null) "Secret not found" else null,
errorNotification = if (secret == null) {
WallencException.Feature.SecretNotFound.toUserNotification()
} else {
null
},
)
}.collect { ui ->
updateState(ui)
@@ -89,7 +95,7 @@ class TextSecretDetailsViewModel @Inject constructor(
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
updateState(
state.value.copy(
errorMessage = "Откройте расшифрованное отображение storage для просмотра и редактирования секрета",
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
),
)
return@launch

View File

@@ -33,6 +33,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretEntryRecord
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.resources.resolveText
@Composable
fun TextSecretEditScreen(
@@ -42,7 +43,7 @@ fun TextSecretEditScreen(
) {
val uiState by viewModel.state.collectAsStateWithLifecycle()
val currentOnSaved by rememberUpdatedState(onSaved)
val inputEnabled = uiState.isAvailable && !uiState.isMutating && uiState.errorMessage == null
val inputEnabled = uiState.isAvailable && !uiState.isMutating && uiState.errorNotification == null
var title by remember(uiState.initialSecret) {
mutableStateOf(uiState.initialSecret?.title.orEmpty())
@@ -75,8 +76,8 @@ fun TextSecretEditScreen(
stringResource(R.string.text_secret_edit)
},
)
uiState.errorMessage?.let { err ->
Text(text = err)
uiState.errorNotification?.let { notification ->
Text(text = notification.resolveText())
}
if (uiState.isMutating) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())

View File

@@ -1,6 +1,7 @@
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.secrets
import androidx.compose.runtime.Immutable
import com.github.nullptroma.wallenc.ui.resources.UserNotification
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
@Immutable
@@ -9,5 +10,5 @@ data class TextSecretEditScreenState(
val isAvailable: Boolean = false,
val isMutating: Boolean = false,
val initialSecret: TextSecretRecord? = null,
val errorMessage: String? = null,
val errorNotification: UserNotification.TextRes? = null,
)

View File

@@ -8,7 +8,9 @@ import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
import com.github.nullptroma.wallenc.domain.tasks.TaskId
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.ui.ViewModelBase
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.optionalSecretId
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.requireStorageUuid
@@ -48,7 +50,7 @@ class TextSecretEditViewModel @Inject constructor(
updateState(
state.value.copy(
isLoading = false,
errorMessage = "Storage not found",
errorNotification = WallencException.Feature.StorageNotFound.toUserNotification(),
),
)
return@launch
@@ -58,7 +60,7 @@ class TextSecretEditViewModel @Inject constructor(
state.value.copy(
isLoading = false,
isAvailable = false,
errorMessage = "Откройте расшифрованное отображение storage для редактирования секрета",
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
),
)
return@launch
@@ -78,7 +80,7 @@ class TextSecretEditViewModel @Inject constructor(
isAvailable = available,
isMutating = isMutating,
initialSecret = currentSecret,
errorMessage = null,
errorNotification = null,
)
}.collect { ui ->
updateState(ui)
@@ -96,7 +98,7 @@ class TextSecretEditViewModel @Inject constructor(
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
updateState(
state.value.copy(
errorMessage = "Откройте расшифрованное отображение storage для редактирования секрета",
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
),
)
return@launch

View File

@@ -33,6 +33,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.resources.resolveText
@Composable
fun TextSecretsScreen(
@@ -66,8 +67,11 @@ fun TextSecretsScreen(
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
uiState.errorMessage?.let {
Text(text = it, color = MaterialTheme.colorScheme.error)
uiState.errorNotification?.let { notification ->
Text(
text = notification.resolveText(),
color = MaterialTheme.colorScheme.error,
)
}
if (uiState.items.isEmpty()) {

View File

@@ -1,6 +1,7 @@
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.secrets
import androidx.compose.runtime.Immutable
import com.github.nullptroma.wallenc.ui.resources.UserNotification
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
@Immutable
@@ -8,5 +9,5 @@ data class TextSecretsScreenState(
val isLoading: Boolean = true,
val isAvailable: Boolean = false,
val items: List<TextSecretRecord> = emptyList(),
val errorMessage: String? = null,
val errorNotification: UserNotification.TextRes? = null,
)

View File

@@ -2,7 +2,9 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.secrets
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.ui.ViewModelBase
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.requireStorageUuid
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
import com.github.nullptroma.wallenc.usecases.ManageTextSecretsUseCase
@@ -31,7 +33,7 @@ class TextSecretsViewModel @Inject constructor(
updateState(
state.value.copy(
isLoading = false,
errorMessage = "Storage not found",
errorNotification = WallencException.Feature.StorageNotFound.toUserNotification(),
),
)
return@launch
@@ -41,7 +43,7 @@ class TextSecretsViewModel @Inject constructor(
state.value.copy(
isLoading = false,
isAvailable = false,
errorMessage = "Откройте расшифрованное отображение storage для работы с секретами",
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
),
)
return@launch
@@ -54,7 +56,7 @@ class TextSecretsViewModel @Inject constructor(
isLoading = false,
isAvailable = available,
items = items,
errorMessage = null,
errorNotification = null,
)
}.collect { ui ->
updateState(ui)

View File

@@ -80,6 +80,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.resources.resolveText
import com.github.nullptroma.wallenc.ui.elements.QrScannerDialog
import com.github.nullptroma.wallenc.usecases.TwoFaCodeState
import com.github.nullptroma.wallenc.usecases.buildTwoFaCodeState
@@ -133,8 +134,11 @@ fun TwoFaTokensScreen(
return@Column
}
uiState.errorMessage?.let {
Text(it)
uiState.errorNotification?.let { notification ->
Text(
text = notification.resolveText(),
color = MaterialTheme.colorScheme.error,
)
}
if (uiState.items.isEmpty()) {

View File

@@ -2,6 +2,7 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.twofa
import androidx.compose.runtime.Immutable
import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
import com.github.nullptroma.wallenc.ui.resources.UserNotification
@Immutable
data class TwoFaTokensScreenState(
@@ -9,5 +10,5 @@ data class TwoFaTokensScreenState(
val isAvailable: Boolean = false,
val isMutating: Boolean = false,
val items: List<TwoFaTokenRecord> = emptyList(),
val errorMessage: String? = null,
val errorNotification: UserNotification.TextRes? = null,
)

View File

@@ -6,7 +6,9 @@ import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.ui.ViewModelBase
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.requireStorageUuid
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
@@ -40,7 +42,7 @@ class TwoFaTokensViewModel @Inject constructor(
updateState(
state.value.copy(
isLoading = false,
errorMessage = "Storage not found",
errorNotification = WallencException.Feature.StorageNotFound.toUserNotification(),
),
)
return@launch
@@ -50,7 +52,7 @@ class TwoFaTokensViewModel @Inject constructor(
state.value.copy(
isLoading = false,
isAvailable = false,
errorMessage = "Откройте расшифрованное отображение storage для работы с 2FA",
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
),
)
return@launch
@@ -69,7 +71,7 @@ class TwoFaTokensViewModel @Inject constructor(
isAvailable = available,
isMutating = isMutating,
items = items,
errorMessage = null,
errorNotification = null,
)
}.collect { ui ->
updateState(ui)
@@ -92,7 +94,7 @@ class TwoFaTokensViewModel @Inject constructor(
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
updateState(
state.value.copy(
errorMessage = "Откройте расшифрованное отображение storage для работы с 2FA",
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
),
)
return@launch
@@ -140,7 +142,7 @@ class TwoFaTokensViewModel @Inject constructor(
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
updateState(
state.value.copy(
errorMessage = "Откройте расшифрованное отображение storage для работы с 2FA",
errorNotification = WallencException.Feature.NeedsDecryptedView.toUserNotification(),
),
)
return@launch

View File

@@ -36,6 +36,7 @@ import com.github.nullptroma.wallenc.domain.tasks.PipelineTask
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -191,7 +192,17 @@ private fun TaskRow(task: PipelineTask, isRunning: Boolean) {
progressLabel ?: stringResource(R.string.task_state_running)
TaskRunState.Completed -> stringResource(R.string.task_state_completed)
TaskRunState.Cancelled -> stringResource(R.string.task_state_cancelled)
is TaskRunState.Failed -> stringResource(R.string.task_state_failed, s.message)
is TaskRunState.Failed -> {
val notification = s.error.toUserNotification()
stringResource(
R.string.task_state_failed,
if (notification.formatArgs.isEmpty()) {
stringResource(notification.id)
} else {
stringResource(notification.id, *notification.formatArgs.toTypedArray())
},
)
}
}
Text(stateLabel, style = MaterialTheme.typography.bodySmall)
if (task.state is TaskRunState.Running) {

View File

@@ -21,8 +21,10 @@ import com.github.nullptroma.wallenc.usecases.StorageFileManagementUseCase
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.ViewModelBase
import com.github.nullptroma.wallenc.ui.extensions.toPrintable
import com.github.nullptroma.wallenc.domain.errors.toWallencException
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
import com.github.nullptroma.wallenc.ui.resources.UserNotification
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -266,12 +268,7 @@ abstract class AbstractVaultBrowserViewModel(
}
} catch (e: Exception) {
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to enable encryption")
_userNotifications.emit(
UserNotification.TextRes(
R.string.msg_failed_enable_encryption,
listOf(e.message ?: e.toString()),
),
)
emitTaskError(e)
}
},
)
@@ -296,12 +293,7 @@ abstract class AbstractVaultBrowserViewModel(
ctx.log(TaskLogLevel.Info, "Storage opened")
} catch (e: Exception) {
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to open encrypted storage")
_userNotifications.emit(
UserNotification.TextRes(
R.string.msg_failed_open_storage,
listOf(e.message ?: e.toString()),
),
)
emitTaskError(e)
}
},
)
@@ -325,12 +317,7 @@ abstract class AbstractVaultBrowserViewModel(
ctx.log(TaskLogLevel.Info, "Storage closed")
} catch (e: Exception) {
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to close encrypted storage")
_userNotifications.emit(
UserNotification.TextRes(
R.string.msg_failed_close_storage,
listOf(e.message ?: e.toString()),
),
)
emitTaskError(e)
}
},
)
@@ -359,12 +346,7 @@ abstract class AbstractVaultBrowserViewModel(
_userNotifications.emit(UserNotification.TextRes(R.string.msg_encryption_disabled))
} catch (e: Exception) {
ctx.log(TaskLogLevel.Error, e.message ?: "Failed")
_userNotifications.emit(
UserNotification.TextRes(
R.string.msg_failed_disable_encryption,
listOf(e.message ?: e.toString()),
),
)
emitTaskError(e)
}
},
)
@@ -464,17 +446,16 @@ abstract class AbstractVaultBrowserViewModel(
_userNotifications.emit(UserNotification.TextRes(R.string.msg_sync_lock_cleared))
} catch (e: Exception) {
ctx.log(TaskLogLevel.Error, e.message ?: "clear sync lock failed")
_userNotifications.emit(
UserNotification.TextRes(
R.string.msg_sync_lock_clear_failed,
listOf(e.message ?: e.toString()),
),
)
emitTaskError(e)
}
},
)
}
private suspend fun emitTaskError(e: Exception) {
_userNotifications.emit(e.toWallencException().toUserNotification())
}
private companion object {
private const val TAG = "VaultBrowser"
}

View File

@@ -154,6 +154,29 @@
<string name="task_title_save_text_secret">Сохранение текстового секрета</string>
<string name="task_title_delete_text_secret">Удаление текстового секрета</string>
<string name="error_storage_not_found">Хранилище не найдено</string>
<string name="error_storage_locked_view">Откройте расшифрованное отображение storage для работы с этим разделом</string>
<string name="error_secret_not_found">Секрет не найден</string>
<string name="error_storage_not_writable">Хранилище недоступно для записи</string>
<string name="error_file_not_found">Файл не найден</string>
<string name="error_incorrect_password">Неверный пароль</string>
<string name="error_storage_not_encrypted">Хранилище не зашифровано</string>
<string name="error_enc_info_missing">Отсутствуют метаданные шифрования</string>
<string name="error_delete_root_forbidden">Нельзя удалить корень хранилища</string>
<string name="error_not_a_file">Ожидался файл</string>
<string name="error_not_a_directory">Ожидалась папка</string>
<string name="error_path_is_file">Путь указывает на файл, а не на папку</string>
<string name="error_cannot_write_over_directory">Нельзя записать поверх папки</string>
<string name="error_unexpected_state">Хранилище в неожиданном состоянии</string>
<string name="error_network">Ошибка сети или сервера</string>
<string name="error_disk_resource_locked">Ресурс временно заблокирован. Повторите позже.</string>
<string name="error_unknown">Что-то пошло не так</string>
<string name="sync_error_group_not_found">Группа синхронизации не найдена</string>
<string name="vault_link_error_auth">Не удалось войти</string>
<string name="vault_link_error_not_registered">Вход не готов. Перезапустите приложение.</string>
<string name="vault_link_error_unknown">Не удалось войти</string>
<string name="vault_link_error_unsupported_brand">Этот провайдер не поддерживается</string>
<string name="msg_encryption_enabled">Шифрование включено</string>
<string name="msg_storage_already_encrypted">Хранилище уже зашифровано</string>
<string name="msg_storage_not_empty">Хранилище не пустое</string>

View File

@@ -0,0 +1,27 @@
package com.github.nullptroma.wallenc.ui.resources
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.ui.R
import org.junit.Assert.assertEquals
import org.junit.Test
class WallencUserNotificationMappingTest {
@Test
fun mapsFeatureStorageNotFound() {
val notification = WallencException.Feature.StorageNotFound.toUserNotification()
assertEquals(R.string.error_storage_not_found, notification.id)
}
@Test
fun mapsStorageIncorrectKey() {
val notification = WallencException.Storage.IncorrectKey.toUserNotification()
assertEquals(R.string.error_incorrect_password, notification.id)
}
@Test
fun mapsUnknown() {
val notification = WallencException.Unknown(Exception("x")).toUserNotification()
assertEquals(R.string.error_unknown, notification.id)
}
}

View File

@@ -1,5 +1,6 @@
package com.github.nullptroma.wallenc.usecases
import com.github.nullptroma.wallenc.domain.errors.toWallencException
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncEngine
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
import com.github.nullptroma.wallenc.domain.tasks.TaskId
@@ -46,8 +47,10 @@ class RunStorageSyncUseCase(
ctx.log(TaskLogLevel.Info, "Storage sync finished")
ctx.reportProgress(null, "Storage sync: completed")
} catch (e: Exception) {
ctx.log(TaskLogLevel.Error, "Storage sync failed: ${e.message}")
ctx.reportProgress(null, "Storage sync: failed - ${e.message}")
val err = e.toWallencException()
ctx.log(TaskLogLevel.Error, "Storage sync failed: $err")
ctx.reportProgress(null, "Storage sync: failed - $err")
ctx.fail(err)
} finally {
running.set(false)
_syncRunning.value = false

View File

@@ -0,0 +1,8 @@
package com.github.nullptroma.wallenc.vault.contract
enum class VaultLinkFailure {
UnsupportedBrand,
NotRegistered,
AuthError,
Unknown,
}

View File

@@ -13,6 +13,6 @@ sealed interface VaultLinkOutcome {
/** Пользователь отменил вход. */
data object Cancelled : VaultLinkOutcome
/** Ошибка SDK/сети/сервера; [message] годится для показа пользователю. */
data class Failed(val message: String) : VaultLinkOutcome
/** Ошибка SDK/сети/сервера; [reason] маппится в UI через [VaultLinkFailure]. */
data class Failed(val reason: VaultLinkFailure) : VaultLinkOutcome
}