diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/UnlockManager.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/UnlockManager.kt new file mode 100644 index 0000000..b3d709c --- /dev/null +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/UnlockManager.kt @@ -0,0 +1,176 @@ +package com.github.nullptroma.wallenc.data.vaults + +import com.github.nullptroma.wallenc.data.db.app.repository.StorageKeyMapRepository +import com.github.nullptroma.wallenc.data.model.StorageKeyMap +import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey +import com.github.nullptroma.wallenc.data.storages.encrypt.EncryptedStorage +import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo +import com.github.nullptroma.wallenc.domain.encrypt.Encryptor +import com.github.nullptroma.wallenc.domain.enums.VaultType +import com.github.nullptroma.wallenc.domain.interfaces.IStorage +import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager +import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.withContext +import java.util.UUID + +class UnlockManager( + private val keymapRepository: StorageKeyMapRepository, + private val ioDispatcher: CoroutineDispatcher, + vaultsManager: IVaultsManager +) : IUnlockManager { + private val _openedStorages = MutableStateFlow?>(null) + override val openedStorages: StateFlow?> + get() = _openedStorages + private val mutex = Mutex() + override val type: VaultType + get() = VaultType.DECRYPTED + override val uuid: UUID + get() = TODO("Not yet implemented") + override val isAvailable: StateFlow + get() = MutableStateFlow(true) + override val totalSpace: StateFlow + get() = MutableStateFlow(null) + override val availableSpace: StateFlow + get() = MutableStateFlow(null) + + override val storages: StateFlow?> + get() = openedStorages.map { it?.values?.toList() }.stateIn( + scope = CoroutineScope(ioDispatcher), + started = SharingStarted.WhileSubscribed(5000L), + initialValue = null + ) + + init { + CoroutineScope(ioDispatcher).launch { + vaultsManager.allStorages.collectLatest { + mutex.lock() + val allKeys = keymapRepository.getAll() + val usedKeys = mutableListOf() + val keysToRemove = mutableListOf() + val allStorages = it.toMutableList() + val map = _openedStorages.value?.toMutableMap() ?: mutableMapOf() + while(allStorages.size > 0) { + val storage = allStorages[allStorages.size-1] + val key = allKeys.find { key -> key.sourceUuid == storage.uuid } + if(key == null) { + allStorages.removeAt(allStorages.size - 1) + continue + } + try { + val encStorage = createEncryptedStorage(storage, key.key, key.destUuid) + map[storage.uuid] = encStorage + usedKeys.add(key) + allStorages.removeAt(allStorages.size - 1) + allStorages.add(encStorage) + } + catch (_: Exception) { + // ключ не подошёл + keysToRemove.add(key) + allStorages.removeAt(allStorages.size - 1) + } + } + keymapRepository.delete(*keysToRemove.toTypedArray()) // удалить мёртвые ключи + _openedStorages.value = map.toMap() + mutex.unlock() + } + } + } + + private suspend fun createEncryptedStorage(storage: IStorage, key: EncryptKey, uuid: UUID): EncryptedStorage { + return EncryptedStorage.create( + source = storage, + key = key, + ioDispatcher = ioDispatcher, + uuid = uuid + ) + } + + override suspend fun open( + storage: IStorage, + key: EncryptKey + ): EncryptedStorage = withContext(ioDispatcher) { + mutex.lock() + val encInfo = storage.metaInfo.value.encInfo ?: throw Exception("EncInfo is null") // TODO + if (!Encryptor.checkKey(key, encInfo)) + throw Exception("Incorrect Key") + + val opened = _openedStorages.first { it != null }!!.toMutableMap() + val cur = opened[storage.uuid] + if (cur != null) + throw Exception("Storage is already open") + + val keymap = StorageKeyMap( + sourceUuid = storage.uuid, + destUuid = UUID.randomUUID(), + key = key + ) + val encStorage = createEncryptedStorage(storage, keymap.key, keymap.destUuid) + opened[storage.uuid] = encStorage + _openedStorages.value = opened + keymapRepository.add(keymap) + mutex.unlock() + return@withContext encStorage + } + + /** + * Закрыть шифрование хранилища, закрывает рекурсивно, удаляя все ключи + * @param storage исходное хранилище, а не расшифрованное отображение + */ + override suspend fun close(storage: IStorage) { + close(storage.uuid) + } + + /** + * Закрыть шифрование хранилища, закрывает рекурсивно, удаляя все ключи + * @param uuid uuid исходного хранилища + */ + override suspend fun close(uuid: UUID): Unit = withContext(ioDispatcher) { + mutex.lock() + val opened = _openedStorages.first { it != null }!! + val enc = opened[uuid] ?: return@withContext + close(enc) + val model = StorageKeyMap( + sourceUuid = uuid, + destUuid = enc.uuid, + key = EncryptKey("") + ) + _openedStorages.value = opened.toMutableMap().apply { + remove(uuid) + } + enc.dispose() + keymapRepository.delete(model) + mutex.unlock() + } + + override suspend fun createStorage(): IStorage { + throw UnsupportedOperationException("Нельзя создать кошелёк на UnlockManager") // TODO + } + + override suspend fun createStorage(enc: StorageEncryptionInfo): IStorage { + throw UnsupportedOperationException("Нельзя создать кошелёк на UnlockManager") // TODO + } + + /** + * Закрыть отображение + * @param storage исходное или расшифрованное хранилище + */ + override suspend fun remove(storage: IStorage) { + val opened = _openedStorages.first { it != null }!! + val source = opened.entries.firstOrNull { + it.key == storage.uuid || it.value.uuid == storage.uuid + } + if(source != null) + close(source.key) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8e92930..2c6df2f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.8.0" +agp = "8.9.1" jacksonModuleKotlin = "2.18.2" kotlin = "2.0.20" coreKtx = "1.15.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f7817a3..279d503 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Sep 07 01:04:14 MSK 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/WallencUi.kt b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/WallencUi.kt index 1b27907..41114b7 100644 --- a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/WallencUi.kt +++ b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/WallencUi.kt @@ -3,8 +3,11 @@ package com.github.nullptroma.wallenc.presentation import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Menu import androidx.compose.material.icons.rounded.Settings @@ -19,6 +22,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -71,13 +75,15 @@ fun WallencNavRoot(viewModel: WallencViewModel = hiltViewModel()) { Scaffold(bottomBar = { - NavigationBar(modifier = Modifier.height(64.dp)) { + NavigationBar(modifier = Modifier.wrapContentHeight()) { val navBackStackEntry by navState.navHostController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route topLevelNavBarItems.forEach { val routeClassName = it.key val navBarItemData = it.value - NavigationBarItem(icon = { + NavigationBarItem( + modifier = Modifier.wrapContentHeight(), + icon = { if (navBarItemData.icon != null) Icon( navBarItemData.icon, contentDescription = stringResource(navBarItemData.nameStringResourceId) @@ -92,7 +98,8 @@ fun WallencNavRoot(viewModel: WallencViewModel = hiltViewModel()) { if (currentRoute?.startsWith(routeClassName) != true) navState.changeTop( route ) - }) + } + ) } } }) { innerPaddings -> diff --git a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/MainScreen.kt b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/MainScreen.kt index 3f5a110..129ccbc 100644 --- a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/MainScreen.kt +++ b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/MainScreen.kt @@ -3,6 +3,7 @@ package com.github.nullptroma.wallenc.presentation.screens.main import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight @@ -17,6 +18,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -62,15 +64,14 @@ fun MainScreen( Scaffold(modifier = modifier, contentWindowInsets = WindowInsets(0.dp), bottomBar = { Column { - NavigationBar(modifier = Modifier.height(48.dp)) { + NavigationBar(windowInsets = WindowInsets(0), modifier = Modifier.height(48.dp)) { val navBackStackEntry by navState.navHostController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route topLevelNavBarItems.forEach { val routeClassName = it.key val navBarItemData = it.value NavigationBarItem(modifier = Modifier - .weight(1f) - .fillMaxHeight(), + .weight(1f), icon = { Text(stringResource(navBarItemData.nameStringResourceId)) }, selected = currentRoute?.startsWith(routeClassName) == true, onClick = { @@ -80,7 +81,9 @@ fun MainScreen( navState.changeTop( route ) - }) + }, + label = null + ) } } HorizontalDivider()