diff --git a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultViewModel.kt b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultViewModel.kt index d93edf2..b1d7ff8 100644 --- a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultViewModel.kt +++ b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultViewModel.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch +import java.util.UUID import javax.inject.Inject import kotlin.system.measureTimeMillis @@ -92,23 +93,32 @@ class LocalVaultViewModel @Inject constructor( } fun printStorageInfoToLog(storage: IStorageInfo) { - storageFileManagementUseCase.setStorage(storage) - viewModelScope.launch { - val files: List - val dirs: List - val time = measureTimeMillis { - files = storageFileManagementUseCase.getAllFiles() - dirs = storageFileManagementUseCase.getAllDirs() - } - for (file in files) { - logger.debug("Files", file.metaInfo.toString()) - } - for (dir in dirs) { - logger.debug("Dirs", dir.metaInfo.toString()) - } - logger.debug("Time", "Time: $time ms") - logger.debug("Storage", storage.toPrintable()) - } + taskOrchestrator.enqueue( + title = "Dump storage to log", + dispatcher = Dispatchers.IO, + work = { ctx -> + storageFileManagementUseCase.setStorage(storage) + ctx.log(TaskLogLevel.Info, "Enumerating files and directories…") + val files: List + val dirs: List + val time = measureTimeMillis { + files = storageFileManagementUseCase.getAllFiles() + dirs = storageFileManagementUseCase.getAllDirs() + } + for (file in files) { + logger.debug("Files", file.metaInfo.toString()) + } + for (dir in dirs) { + logger.debug("Dirs", dir.metaInfo.toString()) + } + logger.debug("Time", "Time: $time ms") + logger.debug("Storage", storage.toPrintable()) + ctx.log( + TaskLogLevel.Info, + "Done: ${files.size} files, ${dirs.size} dirs in ${time}ms (see app log for lines)", + ) + }, + ) } fun createStorage() { @@ -123,71 +133,105 @@ class LocalVaultViewModel @Inject constructor( ) } - private val runningStorages = mutableSetOf() + private val storageOpMutex = Any() + private val runningStorages = mutableSetOf() + fun enableEncryption(storage: IStorageInfo, password: String, encryptPath: Boolean) { val id = storage.uuid - if (runningStorages.contains(id)) - return - tasksCount++ - runningStorages.add(id) + synchronized(storageOpMutex) { + if (runningStorages.contains(id)) return + runningStorages.add(id) + tasksCount++ + } val key = EncryptKey(password) - viewModelScope.launch { - try { - when (manageStoragesEncryptionUseCase.canEncrypt(storage)) { - ManageStoragesEncryptionUseCase.CanEncryptResult.Allowed -> { - manageStoragesEncryptionUseCase.enableEncryption(storage, key, encryptPath) - manageStoragesEncryptionUseCase.openStorage(storage, key, true) - _messages.emit("Encryption enabled") + taskOrchestrator.enqueue( + title = "Enable encryption", + dispatcher = Dispatchers.IO, + work = { ctx -> + try { + ctx.log(TaskLogLevel.Info, "Checking storage…") + when (manageStoragesEncryptionUseCase.canEncrypt(storage)) { + ManageStoragesEncryptionUseCase.CanEncryptResult.Allowed -> { + ctx.log(TaskLogLevel.Info, "Encrypting…") + manageStoragesEncryptionUseCase.enableEncryption(storage, key, encryptPath) + manageStoragesEncryptionUseCase.openStorage(storage, key, true) + ctx.log(TaskLogLevel.Info, "Encryption enabled") + _messages.emit("Encryption enabled") + } + ManageStoragesEncryptionUseCase.CanEncryptResult.AlreadyEncrypted -> { + ctx.log(TaskLogLevel.Info, "Storage is already encrypted") + _messages.emit("Storage is already encrypted") + } + ManageStoragesEncryptionUseCase.CanEncryptResult.StorageIsNotEmpty -> { + ctx.log(TaskLogLevel.Info, "Storage is not empty") + _messages.emit("Storage is not empty") + } + ManageStoragesEncryptionUseCase.CanEncryptResult.StorageStateUnknown -> { + ctx.log(TaskLogLevel.Info, "Cannot determine whether storage is empty") + _messages.emit("Cannot determine whether storage is empty") + } + ManageStoragesEncryptionUseCase.CanEncryptResult.UnsupportedStorageType -> { + ctx.log(TaskLogLevel.Info, "Unsupported storage type") + _messages.emit("Unsupported storage type") + } } - ManageStoragesEncryptionUseCase.CanEncryptResult.AlreadyEncrypted -> { - _messages.emit("Storage is already encrypted") - } - ManageStoragesEncryptionUseCase.CanEncryptResult.StorageIsNotEmpty -> { - _messages.emit("Storage is not empty") - } - ManageStoragesEncryptionUseCase.CanEncryptResult.StorageStateUnknown -> { - _messages.emit("Cannot determine whether storage is empty") - } - ManageStoragesEncryptionUseCase.CanEncryptResult.UnsupportedStorageType -> { - _messages.emit("Unsupported storage type") + } catch (e: Exception) { + ctx.log(TaskLogLevel.Error, e.message ?: "Failed to enable encryption") + _messages.emit(e.message ?: "Failed to enable encryption") + } finally { + synchronized(storageOpMutex) { + runningStorages.remove(id) + tasksCount-- } } - } catch (e: Exception) { - _messages.emit(e.message ?: "Failed to enable encryption") - } - finally { - runningStorages.remove(id) - tasksCount-- - } - } + }, + ) } fun openEncryptedStorage(storage: IStorageInfo, password: String, rememberPassword: Boolean) { val id = storage.uuid - if (runningStorages.contains(id)) return - tasksCount++ - runningStorages.add(id) - val key = EncryptKey(password) - viewModelScope.launch { - try { - manageStoragesEncryptionUseCase.openStorage(storage, key, rememberPassword) - } catch (e: Exception) { - _messages.emit(e.message ?: "Failed to open encrypted storage") - } finally { - runningStorages.remove(id) - tasksCount-- - } + synchronized(storageOpMutex) { + if (runningStorages.contains(id)) return + runningStorages.add(id) + tasksCount++ } + val key = EncryptKey(password) + taskOrchestrator.enqueue( + title = "Open encrypted storage", + dispatcher = Dispatchers.IO, + work = { ctx -> + try { + ctx.log(TaskLogLevel.Info, "Opening storage…") + manageStoragesEncryptionUseCase.openStorage(storage, key, rememberPassword) + ctx.log(TaskLogLevel.Info, "Storage opened") + } catch (e: Exception) { + ctx.log(TaskLogLevel.Error, e.message ?: "Failed to open encrypted storage") + _messages.emit(e.message ?: "Failed to open encrypted storage") + } finally { + synchronized(storageOpMutex) { + runningStorages.remove(id) + tasksCount-- + } + } + }, + ) } fun closeEncryptedStorage(storage: IStorageInfo) { - viewModelScope.launch { - try { - manageStoragesEncryptionUseCase.closeStorage(storage) - } catch (e: Exception) { - _messages.emit(e.message ?: "Failed to close encrypted storage") - } - } + taskOrchestrator.enqueue( + title = "Close encrypted storage", + dispatcher = Dispatchers.IO, + work = { ctx -> + try { + ctx.log(TaskLogLevel.Info, "Closing storage…") + manageStoragesEncryptionUseCase.closeStorage(storage) + ctx.log(TaskLogLevel.Info, "Storage closed") + } catch (e: Exception) { + ctx.log(TaskLogLevel.Error, e.message ?: "Failed to close encrypted storage") + _messages.emit(e.message ?: "Failed to close encrypted storage") + } + }, + ) } fun disableEncryption(storage: IStorageInfo) { @@ -211,9 +255,19 @@ class LocalVaultViewModel @Inject constructor( } fun rename(storage: IStorageInfo, newName: String) { - viewModelScope.launch { - renameStorageUseCase.rename(storage, newName) - } + taskOrchestrator.enqueue( + title = "Rename storage", + dispatcher = Dispatchers.IO, + work = { ctx -> + try { + ctx.log(TaskLogLevel.Info, "Renaming…") + renameStorageUseCase.rename(storage, newName) + ctx.log(TaskLogLevel.Info, "Renamed") + } catch (e: Exception) { + ctx.log(TaskLogLevel.Error, e.message ?: "Rename failed") + } + }, + ) } fun remove(storage: IStorageInfo) { diff --git a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/remotes/RemoteVaultsViewModel.kt b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/remotes/RemoteVaultsViewModel.kt index d104350..9d02306 100644 --- a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/remotes/RemoteVaultsViewModel.kt +++ b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/remotes/RemoteVaultsViewModel.kt @@ -4,18 +4,22 @@ import androidx.lifecycle.viewModelScope import com.github.nullptroma.wallenc.domain.auth.RemoteYandexSignInLauncher import com.github.nullptroma.wallenc.domain.interfaces.IYandexVault import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager +import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator +import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel import com.github.nullptroma.wallenc.presentation.ViewModelBase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel class RemoteVaultsViewModel @Inject constructor( private val vaultsManager: IVaultsManager, val yandexSignIn: RemoteYandexSignInLauncher, + private val taskOrchestrator: ITaskOrchestrator, ) : ViewModelBase(RemoteVaultsScreenState()) { val uiState = combine( @@ -50,15 +54,25 @@ class RemoteVaultsViewModel @Inject constructor( } fun onYandexAuthSuccess(accessToken: String) { - viewModelScope.launch { - setBusy(true) - try { - vaultsManager.addYandexVault(accessToken) - } finally { - setBusy(false) - setAddChoiceVisible(false) - } - } + setBusy(true) + taskOrchestrator.enqueue( + title = "Add Yandex vault", + dispatcher = Dispatchers.IO, + work = { ctx -> + try { + ctx.log(TaskLogLevel.Info, "Adding vault…") + vaultsManager.addYandexVault(accessToken) + ctx.log(TaskLogLevel.Info, "Vault added") + } catch (e: Exception) { + ctx.log(TaskLogLevel.Error, e.message ?: "Failed to add vault") + } finally { + withContext(Dispatchers.Main.immediate) { + setBusy(false) + setAddChoiceVisible(false) + } + } + }, + ) } fun requestDeleteVault(item: RemoteVaultListItem) { @@ -71,14 +85,25 @@ class RemoteVaultsViewModel @Inject constructor( fun confirmDeleteVault() { val pending = state.value.vaultPendingDelete ?: return - viewModelScope.launch { - setBusy(true) - try { - vaultsManager.removeRemoteVault(pending.uuid) - } finally { - setBusy(false) - dismissDeleteVault() - } - } + val uuid = pending.uuid + setBusy(true) + taskOrchestrator.enqueue( + title = "Remove remote vault", + dispatcher = Dispatchers.IO, + work = { ctx -> + try { + ctx.log(TaskLogLevel.Info, "Removing remote vault…") + vaultsManager.removeRemoteVault(uuid) + ctx.log(TaskLogLevel.Info, "Remote vault removed") + } catch (e: Exception) { + ctx.log(TaskLogLevel.Error, e.message ?: "Failed to remove vault") + } finally { + withContext(Dispatchers.Main.immediate) { + setBusy(false) + dismissDeleteVault() + } + } + }, + ) } }