Полное управление шифрованием и ключами
This commit is contained in:
@@ -43,7 +43,9 @@ class UseCasesModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideManageStoragesEncryptionUseCase(unlockManager: IUnlockManager): ManageStoragesEncryptionUseCase {
|
||||
fun provideManageStoragesEncryptionUseCase(
|
||||
unlockManager: IUnlockManager,
|
||||
): ManageStoragesEncryptionUseCase {
|
||||
return ManageStoragesEncryptionUseCase(unlockManager)
|
||||
}
|
||||
|
||||
@@ -52,7 +54,8 @@ class UseCasesModule {
|
||||
fun provideRemoveStorageUseCase(
|
||||
vaultsManager: IVaultsManager,
|
||||
unlockManager: IUnlockManager,
|
||||
manageStoragesEncryptionUseCase: ManageStoragesEncryptionUseCase,
|
||||
): RemoveStorageUseCase {
|
||||
return RemoveStorageUseCase(vaultsManager, unlockManager)
|
||||
return RemoveStorageUseCase(vaultsManager, unlockManager, manageStoragesEncryptionUseCase)
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ interface IAppDb {
|
||||
val storageMetaInfoDao: StorageMetaInfoDao
|
||||
}
|
||||
|
||||
@Database(entities = [DbStorageKeyMap::class, DbStorageMetaInfo::class], version = 2, exportSchema = false)
|
||||
@Database(entities = [DbStorageKeyMap::class, DbStorageMetaInfo::class], version = 3, exportSchema = false)
|
||||
abstract class AppDb : IAppDb, RoomDatabase() {
|
||||
abstract override val storageKeyMapDao: StorageKeyMapDao
|
||||
abstract override val storageMetaInfoDao: StorageMetaInfoDao
|
||||
|
||||
@@ -7,16 +7,15 @@ import com.github.nullptroma.wallenc.data.model.StorageKeyMap
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
|
||||
import java.util.UUID
|
||||
|
||||
@Entity(tableName = "storage_key_maps", primaryKeys = [ "source_uuid", "dest_uuid" ])
|
||||
@Entity(tableName = "storage_key_maps")
|
||||
data class DbStorageKeyMap(
|
||||
@androidx.room.PrimaryKey
|
||||
@ColumnInfo(name = "source_uuid") val sourceUuid: UUID,
|
||||
@ColumnInfo(name = "dest_uuid") val destUuid: UUID,
|
||||
@ColumnInfo(name = "key") val key: ByteArray
|
||||
) {
|
||||
fun toModel(): StorageKeyMap {
|
||||
return StorageKeyMap(
|
||||
sourceUuid = sourceUuid,
|
||||
destUuid = destUuid,
|
||||
key = EncryptKey(key)
|
||||
)
|
||||
}
|
||||
@@ -28,7 +27,6 @@ data class DbStorageKeyMap(
|
||||
other as DbStorageKeyMap
|
||||
|
||||
if (sourceUuid != other.sourceUuid) return false
|
||||
if (destUuid != other.destUuid) return false
|
||||
if (!key.contentEquals(other.key)) return false
|
||||
|
||||
return true
|
||||
@@ -36,7 +34,6 @@ data class DbStorageKeyMap(
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = sourceUuid.hashCode()
|
||||
result = 31 * result + destUuid.hashCode()
|
||||
result = 31 * result + key.contentHashCode()
|
||||
return result
|
||||
}
|
||||
@@ -45,7 +42,6 @@ data class DbStorageKeyMap(
|
||||
fun fromModel(keymap: StorageKeyMap): DbStorageKeyMap {
|
||||
return DbStorageKeyMap(
|
||||
sourceUuid = keymap.sourceUuid,
|
||||
destUuid = keymap.destUuid,
|
||||
key = keymap.key.bytes
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,5 @@ import java.util.UUID
|
||||
|
||||
data class StorageKeyMap(
|
||||
val sourceUuid: UUID,
|
||||
val destUuid: UUID,
|
||||
val key: EncryptKey
|
||||
)
|
||||
|
||||
@@ -17,6 +17,8 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.MessageDigest
|
||||
import java.util.UUID
|
||||
|
||||
class UnlockManager(
|
||||
@@ -45,7 +47,7 @@ class UnlockManager(
|
||||
continue
|
||||
}
|
||||
try {
|
||||
val encStorage = createEncryptedStorage(storage, key.key, key.destUuid)
|
||||
val encStorage = createEncryptedStorage(storage, key.key, getDestUuid(storage.uuid))
|
||||
map[storage.uuid] = encStorage
|
||||
allStorages.removeAt(allStorages.size - 1)
|
||||
allStorages.add(encStorage)
|
||||
@@ -72,9 +74,34 @@ class UnlockManager(
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDestUuid(sourceUuid: UUID): UUID {
|
||||
return uuid5(
|
||||
namespace = UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8"), // URL namespace
|
||||
name = "$sourceUuid:open"
|
||||
)
|
||||
}
|
||||
|
||||
private fun uuid5(namespace: UUID, name: String): UUID {
|
||||
val digest = MessageDigest.getInstance("SHA-1")
|
||||
val nsBytes = ByteBuffer.allocate(16)
|
||||
.putLong(namespace.mostSignificantBits)
|
||||
.putLong(namespace.leastSignificantBits)
|
||||
.array()
|
||||
digest.update(nsBytes)
|
||||
digest.update(name.toByteArray(Charsets.UTF_8))
|
||||
val hash = digest.digest()
|
||||
|
||||
hash[6] = (hash[6].toInt() and 0x0f or 0x50).toByte() // version 5
|
||||
hash[8] = (hash[8].toInt() and 0x3f or 0x80).toByte() // RFC 4122 variant
|
||||
|
||||
val bb = ByteBuffer.wrap(hash, 0, 16)
|
||||
return UUID(bb.long, bb.long)
|
||||
}
|
||||
|
||||
override suspend fun open(
|
||||
storage: IStorage,
|
||||
key: EncryptKey
|
||||
key: EncryptKey,
|
||||
rememberPassword: Boolean
|
||||
): EncryptedStorage = withContext(ioDispatcher) {
|
||||
return@withContext mutex.withLock {
|
||||
val encInfo = storage.metaInfo.value.encInfo ?: throw Exception("EncInfo is null") // TODO
|
||||
@@ -84,17 +111,18 @@ class UnlockManager(
|
||||
val opened = _openedStorages.value.toMutableMap()
|
||||
val cur = opened[storage.uuid]
|
||||
if (cur != null)
|
||||
throw Exception("Storage is already open")
|
||||
return@withLock cur
|
||||
|
||||
val keymap = StorageKeyMap(
|
||||
sourceUuid = storage.uuid,
|
||||
destUuid = UUID.randomUUID(),
|
||||
key = key
|
||||
)
|
||||
val encStorage = createEncryptedStorage(storage, keymap.key, keymap.destUuid)
|
||||
val encStorage = createEncryptedStorage(storage, keymap.key, getDestUuid(storage.uuid))
|
||||
opened[storage.uuid] = encStorage
|
||||
_openedStorages.value = opened
|
||||
if (rememberPassword) {
|
||||
keymapRepository.add(keymap)
|
||||
}
|
||||
encStorage
|
||||
}
|
||||
}
|
||||
@@ -111,30 +139,21 @@ class UnlockManager(
|
||||
}
|
||||
}
|
||||
|
||||
// Закрытие отображения по экземпляру (source или decrypted).
|
||||
// Закрытие только по source-экземпляру.
|
||||
override suspend fun close(storage: IStorage) {
|
||||
val opened = _openedStorages.value
|
||||
val source = opened.entries.firstOrNull {
|
||||
it.key == storage.uuid || it.value.uuid == storage.uuid
|
||||
if (opened.containsKey(storage.uuid)) {
|
||||
close(storage.uuid)
|
||||
}
|
||||
if (source != null)
|
||||
close(source.key)
|
||||
}
|
||||
|
||||
private suspend fun closeBySourceUuid(opened: MutableMap<UUID, EncryptedStorage>, sourceUuid: UUID) {
|
||||
val enc = opened[sourceUuid] ?: return
|
||||
val childSourceUuid = opened.entries.firstOrNull { it.value.uuid == enc.uuid }?.key
|
||||
if (childSourceUuid != null) {
|
||||
closeBySourceUuid(opened, childSourceUuid)
|
||||
val nestedSourceUuid = enc.uuid
|
||||
if (nestedSourceUuid != sourceUuid && opened.containsKey(nestedSourceUuid)) {
|
||||
closeBySourceUuid(opened, nestedSourceUuid)
|
||||
}
|
||||
opened.remove(sourceUuid)
|
||||
enc.dispose()
|
||||
keymapRepository.delete(
|
||||
StorageKeyMap(
|
||||
sourceUuid = sourceUuid,
|
||||
destUuid = enc.uuid,
|
||||
key = EncryptKey("")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DisposableHandle
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -35,6 +36,8 @@ class EncryptedStorage private constructor(
|
||||
get() = accessor.size
|
||||
override val numberOfFiles: StateFlow<Int?>
|
||||
get() = accessor.numberOfFiles
|
||||
override val isEmpty: Flow<Boolean?>
|
||||
get() = accessor.numberOfFiles.map { n -> n?.let { it == 0 } }
|
||||
|
||||
private val _metaInfo = MutableStateFlow<IStorageMetaInfo>(
|
||||
CommonStorageMetaInfo()
|
||||
@@ -102,7 +105,7 @@ class EncryptedStorage private constructor(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo) = scope.run {
|
||||
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo?) = scope.run {
|
||||
val curMeta = metaInfo.value
|
||||
updateMetaInfo(
|
||||
CommonStorageMetaInfo(
|
||||
@@ -112,6 +115,20 @@ class EncryptedStorage private constructor(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun clearAllContent() = scope.run {
|
||||
val files = accessor.getAllFiles()
|
||||
val dirs = accessor.getAllDirs()
|
||||
val paths = buildList {
|
||||
addAll(files.map { it.metaInfo.path })
|
||||
addAll(dirs.map { it.metaInfo.path })
|
||||
}
|
||||
.filter { it != "/" && it.isNotBlank() }
|
||||
.sortedByDescending { it.length }
|
||||
for (path in paths) {
|
||||
accessor.delete(path)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
accessor.dispose()
|
||||
job.cancel()
|
||||
|
||||
@@ -7,8 +7,10 @@ import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.InputStream
|
||||
import java.util.UUID
|
||||
@@ -23,6 +25,8 @@ class LocalStorage(
|
||||
get() = accessor.size
|
||||
override val numberOfFiles: StateFlow<Int?>
|
||||
get() = accessor.numberOfFiles
|
||||
override val isEmpty: Flow<Boolean?>
|
||||
get() = accessor.numberOfFiles.map { n -> n?.let { it == 0 } }
|
||||
|
||||
private val _metaInfo = MutableStateFlow<IStorageMetaInfo>(
|
||||
CommonStorageMetaInfo()
|
||||
@@ -82,7 +86,7 @@ class LocalStorage(
|
||||
))
|
||||
}
|
||||
|
||||
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo) = withContext(ioDispatcher) {
|
||||
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo?) = withContext(ioDispatcher) {
|
||||
val curMeta = metaInfo.value
|
||||
updateMetaInfo(CommonStorageMetaInfo(
|
||||
encInfo = encInfo,
|
||||
@@ -90,6 +94,20 @@ class LocalStorage(
|
||||
))
|
||||
}
|
||||
|
||||
override suspend fun clearAllContent() = withContext(ioDispatcher) {
|
||||
val files = accessor.getAllFiles()
|
||||
val dirs = accessor.getAllDirs()
|
||||
val paths = buildList {
|
||||
addAll(files.map { it.metaInfo.path })
|
||||
addAll(dirs.map { it.metaInfo.path })
|
||||
}
|
||||
.filter { it != "/" && it.isNotBlank() }
|
||||
.sortedByDescending { it.length }
|
||||
for (path in paths) {
|
||||
accessor.delete(path)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val STORAGE_INFO_FILE_POSTFIX = ".storage-info"
|
||||
private val jackson = jacksonObjectMapper().apply { findAndRegisterModules() }
|
||||
|
||||
@@ -494,10 +494,15 @@ class LocalStorageAccessor(
|
||||
}
|
||||
|
||||
override suspend fun delete(path: String) = withContext(ioDispatcher) {
|
||||
if (path == "/" || path.isBlank()) {
|
||||
throw IllegalArgumentException("Deleting root path is forbidden")
|
||||
}
|
||||
val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
|
||||
if (pair != null) {
|
||||
pair.file.delete()
|
||||
if (pair.file.isDirectory) pair.file.deleteRecursively()
|
||||
else pair.file.delete()
|
||||
pair.metaFile.delete()
|
||||
scanSizeAndNumOfFiles()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.github.nullptroma.wallenc.domain.enums
|
||||
|
||||
/**
|
||||
* Политика удаления/закрытия хранилища.
|
||||
*
|
||||
* [CLOSE_ENCRYPTED_OVERLAYS_ONLY] — только закрыть расшифрованные представления (overlay),
|
||||
* физические данные не трогаем.
|
||||
*
|
||||
* [REMOVE_PHYSICAL] — удалить физическое хранилище у провайдера (сейчас local vault),
|
||||
* предварительно закрыв все overlay.
|
||||
*/
|
||||
enum class StorageDeletionPolicy {
|
||||
CLOSE_ENCRYPTED_OVERLAYS_ONLY,
|
||||
REMOVE_PHYSICAL,
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.github.nullptroma.wallenc.domain.interfaces
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
@@ -10,6 +11,7 @@ sealed interface IStorageInfo {
|
||||
val isAvailable: StateFlow<Boolean>
|
||||
val size: StateFlow<Long?>
|
||||
val numberOfFiles: StateFlow<Int?>
|
||||
val isEmpty: Flow<Boolean?>
|
||||
val metaInfo: StateFlow<IStorageMetaInfo>
|
||||
val isVirtualStorage: Boolean
|
||||
}
|
||||
@@ -18,7 +20,8 @@ interface IStorage: IStorageInfo {
|
||||
val accessor: IStorageAccessor
|
||||
|
||||
suspend fun rename(newName: String)
|
||||
suspend fun setEncInfo(encInfo: StorageEncryptionInfo)
|
||||
suspend fun setEncInfo(encInfo: StorageEncryptionInfo?)
|
||||
suspend fun clearAllContent()
|
||||
}
|
||||
|
||||
interface IStorageMetaInfo {
|
||||
|
||||
@@ -14,7 +14,7 @@ interface IUnlockManager {
|
||||
*/
|
||||
val openedStorages: StateFlow<Map<UUID, IStorage>>
|
||||
|
||||
suspend fun open(storage: IStorage, key: EncryptKey): IStorage
|
||||
suspend fun open(storage: IStorage, key: EncryptKey, rememberPassword: Boolean = true): IStorage
|
||||
suspend fun close(storage: IStorage)
|
||||
suspend fun close(uuid: UUID): Unit
|
||||
}
|
||||
@@ -5,23 +5,70 @@ import com.github.nullptroma.wallenc.domain.encrypt.Encryptor
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class ManageStoragesEncryptionUseCase(
|
||||
private val unlockManager: IUnlockManager,
|
||||
) {
|
||||
sealed interface CanEncryptResult {
|
||||
data object Allowed : CanEncryptResult
|
||||
data object UnsupportedStorageType : CanEncryptResult
|
||||
data object AlreadyEncrypted : CanEncryptResult
|
||||
data object StorageIsNotEmpty : CanEncryptResult
|
||||
data object StorageStateUnknown : CanEncryptResult
|
||||
}
|
||||
|
||||
suspend fun canEncrypt(storage: IStorageInfo): CanEncryptResult {
|
||||
if (storage !is IStorage) return CanEncryptResult.UnsupportedStorageType
|
||||
if (storage.metaInfo.value.encInfo != null) return CanEncryptResult.AlreadyEncrypted
|
||||
|
||||
val isEmpty = storage.isEmpty.first()
|
||||
return when (isEmpty) {
|
||||
true -> CanEncryptResult.Allowed
|
||||
false -> CanEncryptResult.StorageIsNotEmpty
|
||||
null -> CanEncryptResult.StorageStateUnknown
|
||||
}
|
||||
}
|
||||
|
||||
class ManageStoragesEncryptionUseCase(private val unlockManager: IUnlockManager) {
|
||||
suspend fun enableEncryption(storage: IStorageInfo, key: EncryptKey, encryptPath: Boolean) {
|
||||
when(storage) {
|
||||
is IStorage -> {
|
||||
if(storage.metaInfo.value.encInfo != null)
|
||||
throw Exception() // TODO
|
||||
storage.setEncInfo(Encryptor.generateEncryptionInfo(key, encryptPath))
|
||||
}
|
||||
when (val result = canEncrypt(storage)) {
|
||||
CanEncryptResult.Allowed -> (storage as IStorage).setEncInfo(
|
||||
Encryptor.generateEncryptionInfo(key, encryptPath)
|
||||
)
|
||||
CanEncryptResult.AlreadyEncrypted -> throw IllegalStateException("Storage is already encrypted")
|
||||
CanEncryptResult.StorageIsNotEmpty -> throw IllegalStateException("Storage is not empty")
|
||||
CanEncryptResult.StorageStateUnknown -> throw IllegalStateException("Storage state is unknown")
|
||||
CanEncryptResult.UnsupportedStorageType -> throw IllegalStateException("Unsupported storage type")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openStorage(storage: IStorageInfo, key: EncryptKey): IStorageInfo {
|
||||
when(storage) {
|
||||
is IStorage -> {
|
||||
return unlockManager.open(storage, key)
|
||||
suspend fun openStorage(storage: IStorageInfo, key: EncryptKey, rememberPassword: Boolean): IStorageInfo {
|
||||
if (storage is IStorage) return unlockManager.open(storage, key, rememberPassword)
|
||||
throw IllegalStateException("Unsupported storage type")
|
||||
}
|
||||
|
||||
suspend fun closeStorage(storage: IStorageInfo) {
|
||||
if (storage is IStorage) {
|
||||
unlockManager.close(storage)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun disableEncryption(storage: IStorageInfo) {
|
||||
clearAndDisableEncryption(storage)
|
||||
}
|
||||
|
||||
suspend fun clearAndDisableEncryption(storage: IStorageInfo) {
|
||||
if (storage !is IStorage) return
|
||||
storage.clearAllContent()
|
||||
storage.setEncInfo(null)
|
||||
unlockManager.close(storage)
|
||||
}
|
||||
|
||||
suspend fun changePassword(storage: IStorageInfo, newKey: EncryptKey, encryptPath: Boolean) {
|
||||
if (storage !is IStorage) return
|
||||
if (storage.metaInfo.value.encInfo == null) {
|
||||
throw IllegalStateException("Storage is not encrypted")
|
||||
}
|
||||
storage.setEncInfo(Encryptor.generateEncryptionInfo(newKey, encryptPath))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.github.nullptroma.wallenc.domain.usecases
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.enums.StorageDeletionPolicy
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
|
||||
@@ -10,36 +9,30 @@ import java.util.UUID
|
||||
class RemoveStorageUseCase(
|
||||
private val vaultsManager: IVaultsManager,
|
||||
private val unlockManager: IUnlockManager,
|
||||
private val manageStoragesEncryptionUseCase: ManageStoragesEncryptionUseCase,
|
||||
) {
|
||||
|
||||
suspend fun remove(storage: IStorageInfo, policy: StorageDeletionPolicy) {
|
||||
suspend fun remove(storage: IStorageInfo) {
|
||||
|
||||
if (storage !is IStorage) return
|
||||
|
||||
when (policy) {
|
||||
StorageDeletionPolicy.CLOSE_ENCRYPTED_OVERLAYS_ONLY -> {
|
||||
if (!storage.isVirtualStorage) {
|
||||
unlockManager.close(storage)
|
||||
}
|
||||
StorageDeletionPolicy.REMOVE_PHYSICAL -> {
|
||||
val physical = findPhysicalRootStorage(storage) ?: return
|
||||
unlockManager.close(physical.uuid)
|
||||
vaultsManager.localVault.remove(physical)
|
||||
}
|
||||
}
|
||||
vaultsManager.localVault.remove(storage)
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Поднимается по цепочке overlay (sourceUuid -> decrypted view), пока не дойдёт
|
||||
* до корневого физического storage из [IVaultsManager.localVault].
|
||||
*/
|
||||
private fun findPhysicalRootStorage(storage: IStorage): IStorage? {
|
||||
val locals = vaultsManager.localVault.storages.value ?: return null
|
||||
val parent = findParentStorage(storage) ?: return
|
||||
manageStoragesEncryptionUseCase.clearAndDisableEncryption(parent)
|
||||
}
|
||||
|
||||
|
||||
private fun findParentStorage(storage: IStorage): IStorage? {
|
||||
val opened = unlockManager.openedStorages.value
|
||||
val parentUuid = opened.entries.firstOrNull { it.value.uuid == storage.uuid }?.key ?: return null
|
||||
val locals = vaultsManager.localVault.storages.value.orEmpty()
|
||||
return locals.firstOrNull { it.uuid == parentUuid }
|
||||
?: opened[parentUuid]
|
||||
}
|
||||
|
||||
var id: UUID = storage.uuid
|
||||
while (true) {
|
||||
val parent = opened.entries.firstOrNull { it.value.uuid == id }?.key ?: break
|
||||
id = parent
|
||||
}
|
||||
return locals.firstOrNull { it.uuid == id }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
@@ -21,6 +22,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
@@ -93,3 +95,98 @@ fun ConfirmationCancelOkDialog(onDismiss: () -> Unit, onConfirmation: () -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EncryptionSetupDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirmation: (password: String, encryptPath: Boolean) -> Unit,
|
||||
) {
|
||||
var password by remember { mutableStateOf("") }
|
||||
var encryptPath by remember { mutableStateOf(false) }
|
||||
BasicAlertDialog(onDismissRequest = onDismiss) {
|
||||
Card {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text("Enable encryption", style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TextField(value = password, onValueChange = { password = it }, label = { Text("Password") })
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = encryptPath, onCheckedChange = { encryptPath = it })
|
||||
Text("Encrypt paths")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
Button(modifier = Modifier.weight(1f), onClick = onDismiss) { Text("Cancel") }
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Button(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { onConfirmation(password, encryptPath) },
|
||||
enabled = password.isNotEmpty()
|
||||
) { Text("Apply") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OpenEncryptedStorageDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirmation: (password: String, rememberPassword: Boolean) -> Unit,
|
||||
) {
|
||||
var password by remember { mutableStateOf("") }
|
||||
var rememberPassword by remember { mutableStateOf(false) }
|
||||
BasicAlertDialog(onDismissRequest = onDismiss) {
|
||||
Card {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text("Open encrypted storage", style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TextField(value = password, onValueChange = { password = it }, label = { Text("Password") })
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = rememberPassword, onCheckedChange = { rememberPassword = it })
|
||||
Text("Remember password")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
Button(modifier = Modifier.weight(1f), onClick = onDismiss) { Text("Cancel") }
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Button(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { onConfirmation(password, rememberPassword) },
|
||||
enabled = password.isNotEmpty()
|
||||
) { Text("Open") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun StorageEncryptionActionsDialog(
|
||||
onDismiss: () -> Unit,
|
||||
title: String,
|
||||
isOpened: Boolean,
|
||||
onOpen: () -> Unit,
|
||||
onClose: () -> Unit,
|
||||
onDisable: () -> Unit,
|
||||
) {
|
||||
BasicAlertDialog(onDismissRequest = onDismiss) {
|
||||
Card {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(title, style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
if (isOpened) {
|
||||
Button(onClick = onClose, modifier = Modifier.fillMaxWidth()) { Text("Close") }
|
||||
} else {
|
||||
Button(onClick = onOpen, modifier = Modifier.fillMaxWidth()) { Text("Open") }
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(onClick = onDisable, modifier = Modifier.fillMaxWidth()) { Text("Disable encryption") }
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Button(onClick = onDismiss, modifier = Modifier.fillMaxWidth()) { Text("Done") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@ import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.LockOpen
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
@@ -60,7 +61,12 @@ fun StorageTree(
|
||||
onClick: (Tree<IStorageInfo>) -> Unit,
|
||||
onRename: (Tree<IStorageInfo>, String) -> Unit,
|
||||
onRemove: (Tree<IStorageInfo>) -> Unit,
|
||||
onEncrypt: (Tree<IStorageInfo>) -> Unit,
|
||||
onEncrypt: (Tree<IStorageInfo>, String, Boolean) -> Unit,
|
||||
onOpenEncrypted: (Tree<IStorageInfo>, String, Boolean) -> Unit,
|
||||
onCloseEncrypted: (Tree<IStorageInfo>) -> Unit,
|
||||
onDisableEncryption: (Tree<IStorageInfo>) -> Unit,
|
||||
getStatusText: (Tree<IStorageInfo>) -> String,
|
||||
isEncryptionOpened: (Tree<IStorageInfo>) -> Boolean,
|
||||
) {
|
||||
val cur = tree.value
|
||||
val available by cur.isAvailable.collectAsStateWithLifecycle()
|
||||
@@ -68,6 +74,8 @@ fun StorageTree(
|
||||
val size by cur.size.collectAsStateWithLifecycle()
|
||||
val metaInfo by cur.metaInfo.collectAsStateWithLifecycle()
|
||||
val isAvailable by cur.isAvailable.collectAsStateWithLifecycle()
|
||||
val isEncrypted = metaInfo.encInfo != null
|
||||
val isOpened = isEncryptionOpened(tree)
|
||||
val borderColor =
|
||||
if (cur.isVirtualStorage) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.primary
|
||||
Column(modifier) {
|
||||
@@ -120,10 +128,13 @@ fun StorageTree(
|
||||
modifier = Modifier,
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
Box(modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 0.dp)) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var showRenameDialog by remember { mutableStateOf(false) }
|
||||
var showRemoveConfirmationDiaglog by remember { mutableStateOf(false) }
|
||||
var showRemoveConfirmDialog by remember { mutableStateOf(false) }
|
||||
var showLockDialog by remember { mutableStateOf(false) }
|
||||
var showSetupEncryptionDialog by remember { mutableStateOf(false) }
|
||||
var showOpenEncryptionDialog by remember { mutableStateOf(false) }
|
||||
Box(modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 0.dp)) {
|
||||
IconButton(onClick = { expanded = !expanded }) {
|
||||
Icon(
|
||||
Icons.Default.MoreVert,
|
||||
@@ -145,10 +156,20 @@ fun StorageTree(
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
expanded = false
|
||||
showRemoveConfirmationDiaglog = true;
|
||||
showRemoveConfirmDialog = true
|
||||
},
|
||||
text = { Text(stringResource(R.string.remove)) }
|
||||
)
|
||||
if (!isEncrypted) {
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
expanded = false
|
||||
showSetupEncryptionDialog = true
|
||||
},
|
||||
text = { Text(stringResource(R.string.encrypt)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showRenameDialog) {
|
||||
@@ -163,26 +184,78 @@ fun StorageTree(
|
||||
)
|
||||
}
|
||||
|
||||
if (showRemoveConfirmationDiaglog) {
|
||||
if (showRemoveConfirmDialog) {
|
||||
ConfirmationCancelOkDialog(
|
||||
onDismiss = {
|
||||
showRemoveConfirmationDiaglog = false
|
||||
},
|
||||
onConfirmation = {
|
||||
showRemoveConfirmationDiaglog = false
|
||||
onRemove(tree)
|
||||
},
|
||||
onDismiss = { showRemoveConfirmDialog = false },
|
||||
title = stringResource(
|
||||
R.string.remove_confirmation_dialog,
|
||||
metaInfo.name ?: "<noname>"
|
||||
),
|
||||
onConfirmation = {
|
||||
showRemoveConfirmDialog = false
|
||||
onRemove(tree)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showLockDialog) {
|
||||
StorageEncryptionActionsDialog(
|
||||
onDismiss = { showLockDialog = false },
|
||||
title = metaInfo.name ?: stringResource(R.string.no_name),
|
||||
isOpened = isOpened,
|
||||
onOpen = {
|
||||
showLockDialog = false
|
||||
showOpenEncryptionDialog = true
|
||||
},
|
||||
onClose = {
|
||||
showLockDialog = false
|
||||
onCloseEncrypted(tree)
|
||||
},
|
||||
onDisable = {
|
||||
showLockDialog = false
|
||||
onDisableEncryption(tree)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showSetupEncryptionDialog) {
|
||||
EncryptionSetupDialog(
|
||||
onDismiss = { showSetupEncryptionDialog = false },
|
||||
onConfirmation = { password, encryptPath ->
|
||||
showSetupEncryptionDialog = false
|
||||
onEncrypt(tree, password, encryptPath)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showOpenEncryptionDialog) {
|
||||
OpenEncryptedStorageDialog(
|
||||
onDismiss = { showOpenEncryptionDialog = false },
|
||||
onConfirmation = { password, rememberPassword ->
|
||||
showOpenEncryptionDialog = false
|
||||
onOpenEncrypted(tree, password, rememberPassword)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Button(onClick = { onEncrypt(tree) }, enabled = metaInfo.encInfo == null) {
|
||||
Text("Encrypt")
|
||||
if (isEncrypted) {
|
||||
IconButton(onClick = { showLockDialog = true }) {
|
||||
Icon(
|
||||
if (isOpened) Icons.Default.LockOpen else Icons.Default.Lock,
|
||||
contentDescription = stringResource(R.string.storage_lock_actions)
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(0.dp, 0.dp, 12.dp, 0.dp)
|
||||
.align(Alignment.End),
|
||||
text = getStatusText(tree),
|
||||
textAlign = TextAlign.End,
|
||||
fontSize = 11.sp,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -221,7 +294,12 @@ fun StorageTree(
|
||||
onClick,
|
||||
onRename,
|
||||
onRemove,
|
||||
onEncrypt
|
||||
onEncrypt,
|
||||
onOpenEncrypted,
|
||||
onCloseEncrypted,
|
||||
onDisableEncryption,
|
||||
getStatusText,
|
||||
isEncryptionOpened
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault
|
||||
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
@@ -23,16 +21,18 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.github.nullptroma.wallenc.presentation.elements.StorageTree
|
||||
import com.github.nullptroma.wallenc.presentation.extensions.gesturesDisabled
|
||||
import kotlinx.coroutines.flow.collect
|
||||
|
||||
@Composable
|
||||
fun LocalVaultScreen(
|
||||
@@ -42,6 +42,13 @@ fun LocalVaultScreen(
|
||||
) {
|
||||
|
||||
val uiState by viewModel.state.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.messages.collect { message ->
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
Scaffold(modifier = modifier, contentWindowInsets = WindowInsets(0.dp), floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
@@ -66,9 +73,24 @@ fun LocalVaultScreen(
|
||||
onRemove = { tree ->
|
||||
viewModel.remove(tree.value)
|
||||
},
|
||||
onEncrypt = { tree ->
|
||||
viewModel.enableEncryptionAndOpenStorage(tree.value)
|
||||
}
|
||||
onEncrypt = { tree, password, encryptPath ->
|
||||
viewModel.enableEncryption(tree.value, password, encryptPath)
|
||||
},
|
||||
onOpenEncrypted = { tree, password, remember ->
|
||||
viewModel.openEncryptedStorage(tree.value, password, remember)
|
||||
},
|
||||
onCloseEncrypted = { tree ->
|
||||
viewModel.closeEncryptedStorage(tree.value)
|
||||
},
|
||||
onDisableEncryption = { tree ->
|
||||
viewModel.disableEncryption(tree.value)
|
||||
},
|
||||
getStatusText = { tree ->
|
||||
viewModel.getStorageStatus(tree.value)
|
||||
},
|
||||
isEncryptionOpened = { tree ->
|
||||
viewModel.isEncryptionSessionOpen(tree.value)
|
||||
},
|
||||
)
|
||||
}
|
||||
item {
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.github.nullptroma.wallenc.domain.interfaces.IDirectory
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IFile
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.ILogger
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
||||
import com.github.nullptroma.wallenc.domain.enums.StorageDeletionPolicy
|
||||
import com.github.nullptroma.wallenc.domain.usecases.GetOpenedStoragesUseCase
|
||||
import com.github.nullptroma.wallenc.domain.usecases.ManageLocalVaultUseCase
|
||||
import com.github.nullptroma.wallenc.domain.usecases.ManageStoragesEncryptionUseCase
|
||||
@@ -19,6 +18,8 @@ import com.github.nullptroma.wallenc.presentation.extensions.toPrintable
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.system.measureTimeMillis
|
||||
@@ -33,6 +34,9 @@ class LocalVaultViewModel @Inject constructor(
|
||||
private val renameStorageUseCase: RenameStorageUseCase,
|
||||
private val logger: ILogger
|
||||
) : ViewModelBase<LocalVaultScreenState>(LocalVaultScreenState(listOf(), true)) {
|
||||
private val _messages = MutableSharedFlow<String>()
|
||||
val messages: SharedFlow<String> = _messages
|
||||
|
||||
private var _taskCount: Int = 0
|
||||
private var tasksCount
|
||||
get() = _taskCount
|
||||
@@ -114,37 +118,105 @@ class LocalVaultViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private val runningStorages = mutableSetOf<IStorageInfo>()
|
||||
fun enableEncryptionAndOpenStorage(storage: IStorageInfo) {
|
||||
if(runningStorages.contains(storage))
|
||||
private val runningStorages = mutableSetOf<java.util.UUID>()
|
||||
fun enableEncryption(storage: IStorageInfo, password: String, encryptPath: Boolean) {
|
||||
val id = storage.uuid
|
||||
if (runningStorages.contains(id))
|
||||
return
|
||||
tasksCount++
|
||||
runningStorages.add(storage)
|
||||
val key = EncryptKey("Hello")
|
||||
runningStorages.add(id)
|
||||
val key = EncryptKey(password)
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
manageStoragesEncryptionUseCase.enableEncryption(storage, key, false)
|
||||
manageStoragesEncryptionUseCase.openStorage(storage, key)
|
||||
when (manageStoragesEncryptionUseCase.canEncrypt(storage)) {
|
||||
ManageStoragesEncryptionUseCase.CanEncryptResult.Allowed -> {
|
||||
manageStoragesEncryptionUseCase.enableEncryption(storage, key, encryptPath)
|
||||
manageStoragesEncryptionUseCase.openStorage(storage, key, true)
|
||||
_messages.emit("Encryption enabled")
|
||||
}
|
||||
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) {
|
||||
_messages.emit(e.message ?: "Failed to enable encryption")
|
||||
}
|
||||
finally {
|
||||
runningStorages.remove(storage)
|
||||
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--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun closeEncryptedStorage(storage: IStorageInfo) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
manageStoragesEncryptionUseCase.closeStorage(storage)
|
||||
} catch (e: Exception) {
|
||||
_messages.emit(e.message ?: "Failed to close encrypted storage")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun disableEncryption(storage: IStorageInfo) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
manageStoragesEncryptionUseCase.disableEncryption(storage)
|
||||
_messages.emit("Encryption disabled")
|
||||
} catch (e: Exception) {
|
||||
_messages.emit(e.message ?: "Failed to disable encryption")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun rename(storage: IStorageInfo, newName: String) {
|
||||
viewModelScope.launch {
|
||||
renameStorageUseCase.rename(storage, newName)
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(
|
||||
storage: IStorageInfo,
|
||||
policy: StorageDeletionPolicy = StorageDeletionPolicy.REMOVE_PHYSICAL,
|
||||
) {
|
||||
fun remove(storage: IStorageInfo) {
|
||||
viewModelScope.launch {
|
||||
removeStorageUseCase.remove(storage, policy)
|
||||
removeStorageUseCase.remove(storage)
|
||||
}
|
||||
}
|
||||
|
||||
fun getStorageStatus(storage: IStorageInfo): String {
|
||||
val encrypted = storage.metaInfo.value.encInfo != null
|
||||
if (!encrypted) return "Not encrypted"
|
||||
val opened = isEncryptionSessionOpen(storage)
|
||||
return if (opened) "Encrypted (opened)" else "Encrypted (closed)"
|
||||
}
|
||||
|
||||
fun isEncryptionSessionOpen(storage: IStorageInfo): Boolean {
|
||||
val openedMap = getOpenedStoragesUseCase.openedStorages.value
|
||||
return openedMap.containsKey(storage.uuid)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,9 @@
|
||||
<string name="show_storage_item_menu">Show storage item menu</string>
|
||||
<string name="rename">Rename</string>
|
||||
<string name="remove">Remove</string>
|
||||
<string name="encrypt">Encrypt</string>
|
||||
<string name="new_name_title">New name</string>
|
||||
<string name="remove_confirmation_dialog">Delete storage "%1$s"?</string>
|
||||
<string name="storage_lock_actions">Storage encryption actions</string>
|
||||
|
||||
</resources>
|
||||
Reference in New Issue
Block a user