refactor(errors): унифицировал доменные ошибки и добавил failed-статус задач
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 -> { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.github.nullptroma.wallenc.vault.contract
|
||||
|
||||
enum class VaultLinkFailure {
|
||||
UnsupportedBrand,
|
||||
NotRegistered,
|
||||
AuthError,
|
||||
Unknown,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user