From ad985679ee00e3d44ab7d09873e2e15951a76e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=8B=D1=82=D0=BA=D0=BE=D0=B2=20=D0=A0=D0=BE=D0=BC?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Sun, 3 May 2026 22:03:47 +0300 Subject: [PATCH] =?UTF-8?q?Yandex=20=D1=88=D1=82=D1=83=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yandexdisk/YandexDiskExceptions.kt | 5 +++ .../repository/YandexDiskRepository.kt | 31 +++++++++++--- .../data/storages/common/BaseStorage.kt | 4 +- .../storages/yandex/YandexStorageAccessor.kt | 11 +++-- .../wallenc/data/vaults/yandex/YandexVault.kt | 18 +++++--- gradle/libs.versions.toml | 6 +-- .../vault/AbstractVaultBrowserViewModel.kt | 41 +++++++++++++++---- .../main/screens/vault/LocalVaultViewModel.kt | 4 +- .../screens/vault/RemoteVaultViewModel.kt | 7 +++- .../main/screens/vault/VaultBrowserScreen.kt | 21 +++++++--- .../screens/vault/VaultBrowserScreenState.kt | 3 +- 11 files changed, 113 insertions(+), 38 deletions(-) create mode 100644 data/src/main/java/com/github/nullptroma/wallenc/data/network/yandexdisk/YandexDiskExceptions.kt diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/network/yandexdisk/YandexDiskExceptions.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/network/yandexdisk/YandexDiskExceptions.kt new file mode 100644 index 0000000..289ba6f --- /dev/null +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/network/yandexdisk/YandexDiskExceptions.kt @@ -0,0 +1,5 @@ +package com.github.nullptroma.wallenc.data.network.yandexdisk + +import java.io.IOException + +class YandexDiskAuthException(message: String? = null) : IOException(message) diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/network/yandexdisk/repository/YandexDiskRepository.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/network/yandexdisk/repository/YandexDiskRepository.kt index b247400..6b9217d 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/network/yandexdisk/repository/YandexDiskRepository.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/network/yandexdisk/repository/YandexDiskRepository.kt @@ -1,10 +1,13 @@ package com.github.nullptroma.wallenc.data.network.yandexdisk.repository +import android.util.Log import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.github.nullptroma.wallenc.data.network.yandexdisk.YandexDiskApi +import com.github.nullptroma.wallenc.data.network.yandexdisk.YandexDiskAuthException import com.github.nullptroma.wallenc.data.network.yandexdisk.dto.CustomPropertiesPatchDto import com.github.nullptroma.wallenc.data.network.yandexdisk.dto.DiskInfoDto +import com.github.nullptroma.wallenc.data.network.yandexdisk.dto.EmbeddedResourceListDto import com.github.nullptroma.wallenc.data.network.yandexdisk.dto.LinkDto import com.github.nullptroma.wallenc.data.network.yandexdisk.dto.OperationStatusDto import com.github.nullptroma.wallenc.data.network.yandexdisk.dto.ResourceDto @@ -22,8 +25,6 @@ import retrofit2.Response import java.io.IOException import java.io.InputStream -class YandexDiskAuthException(message: String? = null) : IOException(message) - class YandexDiskRepository( private val api: YandexDiskApi, private val rawHttp: okhttp3.OkHttpClient, @@ -36,7 +37,15 @@ class YandexDiskRepository( suspend fun list(path: String, limit: Int, offset: Int, sort: String? = null): ResourceDto = withContext(ioDispatcher) { - wrapAuth { api.listResources(path, limit, offset, sort) } + try { + wrapAuth { api.listResources(path, limit, offset, sort) } + } catch (e: HttpException) { + if (e.code() == 404) { + ResourceDto(embedded = EmbeddedResourceListDto(items = emptyList())) + } else { + throw e + } + } } suspend fun get(path: String): ResourceDto = withContext(ioDispatcher) { @@ -47,7 +56,7 @@ class YandexDiskRepository( val resp = wrapAuth { api.createFolder(path) } when (resp.code()) { 201 -> Unit - 409 -> Unit // уже существует + 409 -> Unit else -> throw failure("createFolder", resp) } } @@ -170,8 +179,17 @@ class YandexDiskRepository( try { return block() } catch (e: HttpException) { - if (e.code() == 401) throw YandexDiskAuthException(e.message()) - throw e + when (e.code()) { + 401 -> { + Log.w(TAG, "Disk API 401: ${e.message()}") + throw YandexDiskAuthException(e.message()) + } + 404 -> throw e + else -> { + Log.w(TAG, "Disk API HTTP ${e.code()}: ${e.message()}") + throw e + } + } } } @@ -184,6 +202,7 @@ class YandexDiskRepository( jackson.readValue(body.string()) companion object { + private const val TAG = "YandexDiskRepo" private val jackson = jacksonObjectMapper().apply { findAndRegisterModules() } private val OCTET_STREAM = "application/octet-stream".toMediaType() private const val OPERATION_POLL_DELAY_MS = 300L diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/storages/common/BaseStorage.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/storages/common/BaseStorage.kt index 3af8417..951077b 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/storages/common/BaseStorage.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/storages/common/BaseStorage.kt @@ -86,10 +86,8 @@ abstract class BaseStorage( private suspend fun updateMetaInfo(meta: IStorageMetaInfo) = withContext(ioDispatcher) { val writer = accessor.openWriteSystemFile(metaInfoFileName) - try { + writer.use { writer -> jackson.writeValue(writer, meta) - } finally { - writer.close() } _metaInfo.value = meta } diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/storages/yandex/YandexStorageAccessor.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/storages/yandex/YandexStorageAccessor.kt index fe3648c..4a2a4ae 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/storages/yandex/YandexStorageAccessor.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/storages/yandex/YandexStorageAccessor.kt @@ -1,6 +1,6 @@ package com.github.nullptroma.wallenc.data.storages.yandex -import com.github.nullptroma.wallenc.data.network.yandexdisk.repository.YandexDiskAuthException +import com.github.nullptroma.wallenc.data.network.yandexdisk.YandexDiskAuthException import com.github.nullptroma.wallenc.data.network.yandexdisk.dto.ResourceDto import com.github.nullptroma.wallenc.data.network.yandexdisk.repository.YandexDiskRepository import com.github.nullptroma.wallenc.data.utils.CloseHandledStreamExtension.Companion.onClosed @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn +import android.util.Log import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream @@ -72,13 +73,16 @@ class YandexStorageAccessor( try { scanSizeAndNumOfFiles() _storageReady.value = true + Log.d(TAG, "init ok storageUuid=$storageUuid") } catch (e: YandexDiskAuthException) { reportAuthFailure() _storageReady.value = false + Log.w(TAG, "init auth failed storageUuid=$storageUuid", e) throw e - } catch (_: Exception) { + } catch (e: Exception) { _storageReady.value = false - throw Exception("Yandex storage init failed") + Log.w(TAG, "init failed storageUuid=$storageUuid", e) + throw Exception("Yandex storage init failed", e) } } @@ -432,6 +436,7 @@ class YandexStorageAccessor( } companion object { + private const val TAG = "YandexStorageAcc" private const val SYSTEM_HIDDEN_DIRNAME = "wallenc-yandex-system" private const val DATA_PAGE_LENGTH = 10 private const val API_LIST_LIMIT = 200 diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/yandex/YandexVault.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/yandex/YandexVault.kt index e42b935..21b2a67 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/yandex/YandexVault.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/yandex/YandexVault.kt @@ -1,6 +1,6 @@ package com.github.nullptroma.wallenc.data.vaults.yandex -import com.github.nullptroma.wallenc.data.network.yandexdisk.repository.YandexDiskAuthException +import com.github.nullptroma.wallenc.data.network.yandexdisk.YandexDiskAuthException import com.github.nullptroma.wallenc.data.network.yandexdisk.repository.YandexDiskRepository import com.github.nullptroma.wallenc.data.storages.yandex.YandexStorage import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo @@ -12,6 +12,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import android.util.Log import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.UUID @@ -64,12 +65,15 @@ class YandexVault( _availableSpace.value = (total - used).coerceAtLeast(0L) _vaultReachable.value = true _storages.value = loadStoragesList() - } catch (_: YandexDiskAuthException) { + Log.d(TAG, "refresh ok uuid=$uuid storages=${_storages.value.size}") + } catch (e: YandexDiskAuthException) { _vaultReachable.value = false _storages.value = emptyList() - } catch (_: Exception) { + Log.w(TAG, "refresh auth failed uuid=$uuid: ${e.message}") + } catch (e: Exception) { _vaultReachable.value = false _storages.value = emptyList() + Log.w(TAG, "refresh failed uuid=$uuid", e) } } @@ -96,8 +100,8 @@ class YandexVault( try { storage.init() out.add(storage) - } catch (_: Exception) { - // пропускаем битое/частично созданное хранилище + } catch (e: Exception) { + Log.w(TAG, "skip broken storage uuid=$storageUuid: ${e.message}") } } if (items.size < APP_LIST_LIMIT) break @@ -108,6 +112,7 @@ class YandexVault( override suspend fun createStorage(): IStorage = withContext(ioDispatcher) { val id = UUID.randomUUID() + Log.d(TAG, "createStorage start vault=$uuid storage=$id") repo.createFolder("app:/$id") val storage = YandexStorage( uuid = id, @@ -121,6 +126,7 @@ class YandexVault( ) storage.init() _storages.value = _storages.value + storage + Log.d(TAG, "createStorage done storage=$id") storage } @@ -132,11 +138,13 @@ class YandexVault( override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) { if (storage !is YandexStorage) return@withContext + Log.d(TAG, "remove storage=${storage.uuid}") repo.delete("app:/${storage.uuid}", permanently = true) _storages.value = _storages.value.filter { it.uuid != storage.uuid } } private companion object { + private const val TAG = "YandexVault" private const val APP_LIST_LIMIT = 200 } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c30fc36..60426c6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,9 +44,9 @@ room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } # Retrofit -retrofit = { group="com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit"} -retrofit-converter-scalars = { group="com.squareup.retrofit2", name = "converter-scalars", version.ref = "retrofit"} -retrofit-converter-jackson = { group="com.squareup.retrofit2", name = "converter-jackson", version.ref = "retrofit"} +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofit-converter-scalars = { group = "com.squareup.retrofit2", name = "converter-scalars", version.ref = "retrofit" } +retrofit-converter-jackson = { group = "com.squareup.retrofit2", name = "converter-jackson", version.ref = "retrofit" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } junit = { group = "junit", name = "junit", version.ref = "junit" } diff --git a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/AbstractVaultBrowserViewModel.kt b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/AbstractVaultBrowserViewModel.kt index f46a332..9b40917 100644 --- a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/AbstractVaultBrowserViewModel.kt +++ b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/AbstractVaultBrowserViewModel.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import java.util.UUID import kotlin.system.measureTimeMillis @@ -32,7 +33,7 @@ import kotlin.system.measureTimeMillis */ abstract class AbstractVaultBrowserViewModel( storagesFlow: Flow>, - private val canAddStorage: Boolean, + private val vaultAvailabilityFlow: Flow, private val resolveCreateVaultUuid: () -> UUID?, private val removeStorageUseCase: RemoveStorageUseCase, private val getOpenedStoragesUseCase: GetOpenedStoragesUseCase, @@ -43,7 +44,7 @@ abstract class AbstractVaultBrowserViewModel( private val taskOrchestrator: ITaskOrchestrator, private val logger: ILogger, ) : ViewModelBase( - VaultBrowserScreenState(storagesList = emptyList(), isLoading = true, canAddStorage = canAddStorage), + VaultBrowserScreenState(storagesList = emptyList(), isLoading = true, addStorageFabEnabled = false), ) { private val _messages = MutableSharedFlow() @@ -63,6 +64,14 @@ abstract class AbstractVaultBrowserViewModel( init { collectFlows(storagesFlow) + viewModelScope.launch { + vaultAvailabilityFlow + .distinctUntilChanged() + .collect { available -> + updateState(state.value.copy(addStorageFabEnabled = available)) + logger.debug(TAG, "vault availability → add FAB enabled=$available") + } + } } private fun updateStateLoading() { @@ -125,20 +134,36 @@ abstract class AbstractVaultBrowserViewModel( } fun createStorage() { - if (!state.value.canAddStorage) return + if (!state.value.addStorageFabEnabled) { + logger.debug(TAG, "createStorage ignored (vault unavailable or FAB disabled)") + return + } + logger.debug(TAG, "createStorage: enqueue task") taskOrchestrator.enqueue( title = "Create storage", dispatcher = Dispatchers.IO, work = { ctx -> - ctx.log(TaskLogLevel.Info, "Creating storage…") - val uuid = resolveCreateVaultUuid() - ?: throw IllegalStateException("Vault is not available") - manageVaultUseCase.createStorage(uuid) - ctx.log(TaskLogLevel.Info, "Storage created") + try { + ctx.log(TaskLogLevel.Info, "Creating storage…") + val uuid = resolveCreateVaultUuid() + ?: throw IllegalStateException("Vault is not available") + logger.debug(TAG, "createStorage: vaultUuid=$uuid") + val storage = manageVaultUseCase.createStorage(uuid) + ctx.log(TaskLogLevel.Info, "Storage created") + logger.debug(TAG, "createStorage: done storageUuid=${storage.uuid}") + } catch (e: Exception) { + logger.debug(TAG, "createStorage failed: ${e.stackTraceToString()}") + ctx.log(TaskLogLevel.Error, e.message ?: e.toString()) + throw e + } }, ) } + private companion object { + private const val TAG = "VaultBrowser" + } + private val storageOpMutex = Any() private val runningStorages = mutableSetOf() diff --git a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/LocalVaultViewModel.kt b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/LocalVaultViewModel.kt index 67a8cf3..2b7fcc8 100644 --- a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/LocalVaultViewModel.kt +++ b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/LocalVaultViewModel.kt @@ -34,7 +34,9 @@ class LocalVaultViewModel @Inject constructor( storagesFlow = vaultsManager.vaults .map { vaults -> vaults.described().locals.firstOrNull() } .flatMapLatest { v -> v?.storages ?: flowOf(emptyList()) }, - canAddStorage = true, + vaultAvailabilityFlow = vaultsManager.vaults + .map { vaults -> vaults.described().locals.firstOrNull() } + .flatMapLatest { v -> v?.isAvailable ?: flowOf(false) }, resolveCreateVaultUuid = { vaultsManager.vaults.value.described().locals.firstOrNull()?.uuid }, removeStorageUseCase = removeStorageUseCase, getOpenedStoragesUseCase = getOpenedStoragesUseCase, diff --git a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/RemoteVaultViewModel.kt b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/RemoteVaultViewModel.kt index 9950008..0c9b6f5 100644 --- a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/RemoteVaultViewModel.kt +++ b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/RemoteVaultViewModel.kt @@ -11,6 +11,8 @@ import com.github.nullptroma.wallenc.domain.usecases.RenameStorageUseCase import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import java.util.UUID import javax.inject.Inject @@ -28,8 +30,9 @@ class RemoteVaultViewModel @Inject constructor( logger: ILogger, ) : AbstractVaultBrowserViewModel( storagesFlow = manageVaultUseCase.storagesOf(savedStateHandle.requireVaultUuid()), - canAddStorage = false, - resolveCreateVaultUuid = { null }, + vaultAvailabilityFlow = manageVaultUseCase.observe(savedStateHandle.requireVaultUuid()) + .flatMapLatest { v -> v?.isAvailable ?: flowOf(false) }, + resolveCreateVaultUuid = { savedStateHandle.requireVaultUuid() }, removeStorageUseCase = removeStorageUseCase, getOpenedStoragesUseCase = getOpenedStoragesUseCase, storageFileManagementUseCase = storageFileManagementUseCase, diff --git a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/VaultBrowserScreen.kt b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/VaultBrowserScreen.kt index 7ef6ba6..bd19040 100644 --- a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/VaultBrowserScreen.kt +++ b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/VaultBrowserScreen.kt @@ -50,12 +50,21 @@ fun VaultBrowserScreen( modifier = modifier, contentWindowInsets = WindowInsets(0.dp), floatingActionButton = { - if (uiState.canAddStorage) { - FloatingActionButton( - onClick = { viewModel.createStorage() }, - ) { - Icon(Icons.Filled.Add, contentDescription = null) - } + val fabEnabled = uiState.addStorageFabEnabled + FloatingActionButton( + onClick = { if (fabEnabled) viewModel.createStorage() }, + containerColor = if (fabEnabled) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceVariant + }, + contentColor = if (fabEnabled) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.55f) + }, + ) { + Icon(Icons.Filled.Add, contentDescription = null) } }, ) { innerPadding -> diff --git a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/VaultBrowserScreenState.kt b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/VaultBrowserScreenState.kt index d4f88ac..26b60cc 100644 --- a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/VaultBrowserScreenState.kt +++ b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/vault/VaultBrowserScreenState.kt @@ -6,5 +6,6 @@ import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo data class VaultBrowserScreenState( val storagesList: List>, val isLoading: Boolean, - val canAddStorage: Boolean = false, + /** FAB «добавить storage»: активна только когда vault доступен (сеть/API/путь). */ + val addStorageFabEnabled: Boolean = false, )