Compare commits

..

10 Commits

Author SHA1 Message Date
Пытков Роман
ce99888c6e Убран лишний отступ на NavBar на основном экране 2025-03-27 18:48:05 +03:00
Пытков Роман
824306d8bc storages в UnlockManager 2025-02-11 18:17:19 +03:00
Пытков Роман
85b8517a76 IUnlockManager теперь IVault 2025-02-11 17:55:54 +03:00
Пытков Роман
e1646611c2 Поправлен клик сквозь экран загрузки 2025-02-09 22:03:14 +03:00
Пытков Роман
4404ef2ff4 Исправление для юнит теста 2025-02-08 20:57:00 +03:00
Пытков Роман
86b5c6cae2 Опциональное шифрование имён файлов 2025-02-08 20:51:28 +03:00
Пытков Роман
da8808a4b9 Статичный IV для имён файлов 2025-02-08 17:45:13 +03:00
Пытков Роман
c95c374852 Дерево хранилищ 2025-02-05 21:29:16 +03:00
Пытков Роман
2cb2dabe3f Добавлен clickableDebounced 2025-02-05 13:27:20 +03:00
Пытков Роман
f7071382a7 Delete app/release directory 2025-01-28 22:53:14 +03:00
40 changed files with 1083 additions and 376 deletions

View File

@@ -1,37 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.github.nullptroma.wallenc.app",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 26
}

View File

@@ -25,9 +25,17 @@ class SingletonModule {
@Singleton @Singleton
fun provideVaultsManager( fun provideVaultsManager(
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
@ApplicationContext context: Context @ApplicationContext context: Context,
keyRepo: StorageKeyMapRepository,
): IVaultsManager { ): IVaultsManager {
return VaultsManager(ioDispatcher, context) return VaultsManager(ioDispatcher, context, keyRepo)
}
@Provides
fun provideUnlockManager(
vaultsManager: IVaultsManager
): IUnlockManager {
return vaultsManager.unlockManager
} }
@Provides @Provides
@@ -47,20 +55,4 @@ class SingletonModule {
): StorageMetaInfoRepository { ): StorageMetaInfoRepository {
return StorageMetaInfoRepository(dao, ioDispatcher) return StorageMetaInfoRepository(dao, ioDispatcher)
} }
@Provides
@Singleton
fun provideUnlockManager(
@IoDispatcher ioDispatcher: CoroutineDispatcher,
keyRepo: StorageKeyMapRepository,
metaRepo: StorageMetaInfoRepository,
vaultsManager: IVaultsManager
): IUnlockManager {
return UnlockManager(
keymapRepository = keyRepo,
metaInfoRepository = metaRepo,
ioDispatcher = ioDispatcher,
vaultsManager = vaultsManager
)
}
} }

View File

@@ -4,6 +4,7 @@ import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
import com.github.nullptroma.wallenc.domain.usecases.GetOpenedStoragesUseCase import com.github.nullptroma.wallenc.domain.usecases.GetOpenedStoragesUseCase
import com.github.nullptroma.wallenc.domain.usecases.ManageLocalVaultUseCase import com.github.nullptroma.wallenc.domain.usecases.ManageLocalVaultUseCase
import com.github.nullptroma.wallenc.domain.usecases.ManageStoragesEncryptionUseCase
import com.github.nullptroma.wallenc.domain.usecases.RenameStorageUseCase import com.github.nullptroma.wallenc.domain.usecases.RenameStorageUseCase
import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase
import dagger.Module import dagger.Module
@@ -38,4 +39,10 @@ class UseCasesModule {
fun provideRenameStorageUseCase(): RenameStorageUseCase { fun provideRenameStorageUseCase(): RenameStorageUseCase {
return RenameStorageUseCase() return RenameStorageUseCase()
} }
@Provides
@Singleton
fun provideManageStoragesEncryptionUseCase(unlockManager: IUnlockManager): ManageStoragesEncryptionUseCase {
return ManageStoragesEncryptionUseCase(unlockManager)
}
} }

View File

@@ -10,11 +10,11 @@ import com.github.nullptroma.wallenc.data.db.app.model.DbStorageKeyMap
@Dao @Dao
interface StorageKeyMapDao { interface StorageKeyMapDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun add(keymap: DbStorageKeyMap) suspend fun add(vararg keymaps: DbStorageKeyMap)
@Query("SELECT * FROM storage_key_maps") @Query("SELECT * FROM storage_key_maps")
suspend fun getAll(): List<DbStorageKeyMap> suspend fun getAll(): List<DbStorageKeyMap>
@Delete @Delete
suspend fun delete(keymap: DbStorageKeyMap) suspend fun delete(vararg keymaps: DbStorageKeyMap)
} }

View File

@@ -11,13 +11,13 @@ class StorageKeyMapRepository(
private val ioDispatcher: CoroutineDispatcher private val ioDispatcher: CoroutineDispatcher
) { ) {
suspend fun getAll() = withContext(ioDispatcher) { dao.getAll().map { it.toModel() } } suspend fun getAll() = withContext(ioDispatcher) { dao.getAll().map { it.toModel() } }
suspend fun add(keymap: StorageKeyMap) = withContext(ioDispatcher) { suspend fun add(vararg keymaps: StorageKeyMap) = withContext(ioDispatcher) {
val dbModel = DbStorageKeyMap.fromModel(keymap) val dbModels = keymaps.map { DbStorageKeyMap.fromModel(it) }
dao.add(dbModel) dao.add(*dbModels.toTypedArray())
} }
suspend fun delete(keymap: StorageKeyMap) = withContext(ioDispatcher) { suspend fun delete(vararg keymaps: StorageKeyMap) = withContext(ioDispatcher) {
val dbModel = DbStorageKeyMap.fromModel(keymap) val dbModels = keymaps.map { DbStorageKeyMap.fromModel(it) }
dao.delete(dbModel) dao.delete(*dbModels.toTypedArray())
} }
} }

View File

@@ -5,16 +5,22 @@ import com.github.nullptroma.wallenc.data.db.app.repository.StorageMetaInfoRepos
import com.github.nullptroma.wallenc.data.model.StorageKeyMap import com.github.nullptroma.wallenc.data.model.StorageKeyMap
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
import com.github.nullptroma.wallenc.data.storages.encrypt.EncryptedStorage 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.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.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -22,7 +28,6 @@ import java.util.UUID
class UnlockManager( class UnlockManager(
private val keymapRepository: StorageKeyMapRepository, private val keymapRepository: StorageKeyMapRepository,
private val metaInfoRepository: StorageMetaInfoRepository,
private val ioDispatcher: CoroutineDispatcher, private val ioDispatcher: CoroutineDispatcher,
vaultsManager: IVaultsManager vaultsManager: IVaultsManager
) : IUnlockManager { ) : IUnlockManager {
@@ -30,22 +35,55 @@ class UnlockManager(
override val openedStorages: StateFlow<Map<UUID, IStorage>?> override val openedStorages: StateFlow<Map<UUID, IStorage>?>
get() = _openedStorages get() = _openedStorages
private val mutex = Mutex() private val mutex = Mutex()
override val type: VaultType
get() = VaultType.DECRYPTED
override val uuid: UUID
get() = TODO("Not yet implemented")
override val isAvailable: StateFlow<Boolean>
get() = MutableStateFlow(true)
override val totalSpace: StateFlow<Int?>
get() = MutableStateFlow(null)
override val availableSpace: StateFlow<Int?>
get() = MutableStateFlow(null)
override val storages: StateFlow<List<IStorage>?>
get() = openedStorages.map { it?.values?.toList() }.stateIn(
scope = CoroutineScope(ioDispatcher),
started = SharingStarted.WhileSubscribed(5000L),
initialValue = null
)
init { init {
CoroutineScope(ioDispatcher).launch { CoroutineScope(ioDispatcher).launch {
vaultsManager.allStorages.collectLatest { vaultsManager.allStorages.collectLatest {
mutex.lock() mutex.lock()
val allKeys = keymapRepository.getAll() val allKeys = keymapRepository.getAll()
val allStorages = it.associateBy({ it.uuid }, { it }) val usedKeys = mutableListOf<StorageKeyMap>()
val keysToRemove = mutableListOf<StorageKeyMap>()
val allStorages = it.toMutableList()
val map = _openedStorages.value?.toMutableMap() ?: mutableMapOf() val map = _openedStorages.value?.toMutableMap() ?: mutableMapOf()
for(keymap in allKeys) { while(allStorages.size > 0) {
if(map.contains(keymap.sourceUuid)) val storage = allStorages[allStorages.size-1]
val key = allKeys.find { key -> key.sourceUuid == storage.uuid }
if(key == null) {
allStorages.removeAt(allStorages.size - 1)
continue continue
val storage = allStorages[keymap.sourceUuid] ?: continue }
val encStorage = createEncryptedStorage(storage, keymap.key, keymap.destUuid) try {
map[storage.uuid] = encStorage 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)
}
} }
_openedStorages.value = map keymapRepository.delete(*keysToRemove.toTypedArray()) // удалить мёртвые ключи
_openedStorages.value = map.toMap()
mutex.unlock() mutex.unlock()
} }
} }
@@ -56,7 +94,6 @@ class UnlockManager(
source = storage, source = storage,
key = key, key = key,
ioDispatcher = ioDispatcher, ioDispatcher = ioDispatcher,
metaInfoProvider = metaInfoRepository.createSingleStorageProvider(uuid),
uuid = uuid uuid = uuid
) )
} }
@@ -64,7 +101,7 @@ class UnlockManager(
override suspend fun open( override suspend fun open(
storage: IStorage, storage: IStorage,
key: EncryptKey key: EncryptKey
) = withContext(ioDispatcher) { ): EncryptedStorage = withContext(ioDispatcher) {
mutex.lock() mutex.lock()
val encInfo = storage.metaInfo.value.encInfo ?: throw Exception("EncInfo is null") // TODO val encInfo = storage.metaInfo.value.encInfo ?: throw Exception("EncInfo is null") // TODO
if (!Encryptor.checkKey(key, encInfo)) if (!Encryptor.checkKey(key, encInfo))
@@ -85,22 +122,57 @@ class UnlockManager(
_openedStorages.value = opened _openedStorages.value = opened
keymapRepository.add(keymap) keymapRepository.add(keymap)
mutex.unlock() mutex.unlock()
return@withContext encStorage
} }
override suspend fun close(storage: IStorage) = withContext(ioDispatcher) { /**
* Закрыть шифрование хранилища, закрывает рекурсивно, удаляя все ключи
* @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() mutex.lock()
val opened = _openedStorages.first { it != null }!! val opened = _openedStorages.first { it != null }!!
val enc = opened[storage.uuid] ?: return@withContext val enc = opened[uuid] ?: return@withContext
close(enc)
val model = StorageKeyMap( val model = StorageKeyMap(
sourceUuid = storage.uuid, sourceUuid = uuid,
destUuid = enc.uuid, destUuid = enc.uuid,
key = EncryptKey("") key = EncryptKey("")
) )
_openedStorages.value = opened.toMutableMap().apply { _openedStorages.value = opened.toMutableMap().apply {
remove(storage.uuid) remove(uuid)
} }
enc.dispose() enc.dispose()
keymapRepository.delete(model) keymapRepository.delete(model)
mutex.unlock() 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)
}
} }

View File

@@ -1,29 +1,40 @@
package com.github.nullptroma.wallenc.data.storages.encrypt package com.github.nullptroma.wallenc.data.storages.encrypt
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.github.nullptroma.wallenc.data.db.app.repository.StorageMetaInfoRepository import com.github.nullptroma.wallenc.data.db.app.repository.StorageMetaInfoRepository
import com.github.nullptroma.wallenc.domain.common.impl.CommonStorageMetaInfo import com.github.nullptroma.wallenc.domain.common.impl.CommonStorageMetaInfo
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
import com.github.nullptroma.wallenc.domain.encrypt.Encryptor
import com.github.nullptroma.wallenc.domain.interfaces.IStorage import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.InputStream
import java.util.UUID import java.util.UUID
class EncryptedStorage private constructor( class EncryptedStorage private constructor(
private val source: IStorage, private val source: IStorage,
key: EncryptKey, private val key: EncryptKey,
private val ioDispatcher: CoroutineDispatcher, ioDispatcher: CoroutineDispatcher,
private val metaInfoProvider: StorageMetaInfoRepository.SingleStorageMetaInfoProvider,
override val uuid: UUID = UUID.randomUUID() override val uuid: UUID = UUID.randomUUID()
) : IStorage, DisposableHandle { ) : IStorage, DisposableHandle {
private val job = Job()
private val scope = CoroutineScope(ioDispatcher + job)
private val encInfo =
source.metaInfo.value.encInfo ?: throw Exception("Storage is not encrypted") // TODO
private val metaInfoFileName: String = "${uuid.toString().take(8)}$STORAGE_INFO_FILE_POSTFIX"
override val size: StateFlow<Long?> override val size: StateFlow<Long?>
get() = source.size get() = accessor.size
override val numberOfFiles: StateFlow<Int?> override val numberOfFiles: StateFlow<Int?>
get() = source.numberOfFiles get() = accessor.numberOfFiles
private val _metaInfo = MutableStateFlow<IStorageMetaInfo>( private val _metaInfo = MutableStateFlow<IStorageMetaInfo>(
CommonStorageMetaInfo() CommonStorageMetaInfo()
@@ -35,43 +46,75 @@ class EncryptedStorage private constructor(
override val isAvailable: StateFlow<Boolean> override val isAvailable: StateFlow<Boolean>
get() = source.isAvailable get() = source.isAvailable
override val accessor: EncryptedStorageAccessor = override val accessor: EncryptedStorageAccessor =
EncryptedStorageAccessor(source.accessor, key, ioDispatcher) EncryptedStorageAccessor(
source = source.accessor,
pathIv = encInfo.pathIv,
key = key,
systemHiddenDirName = "${uuid.toString().take(8)}$SYSTEM_HIDDEN_DIRNAME_POSTFIX",
scope = scope
)
private suspend fun init() { private suspend fun init() {
readMeta() checkKey()
readMetaInfo()
} }
private suspend fun readMeta() = withContext(ioDispatcher) { private fun checkKey() {
var meta = metaInfoProvider.get() if (!Encryptor.checkKey(key, encInfo))
if(meta == null) { throw Exception("Incorrect key") // TODO
}
private suspend fun readMetaInfo() = scope.run {
var meta: CommonStorageMetaInfo
var reader: InputStream? = null
try {
reader = accessor.openReadSystemFile(metaInfoFileName)
meta = jackson.readValue(reader, CommonStorageMetaInfo::class.java)
} catch (e: Exception) {
// чтение не удалось, значит нужно записать файл
meta = CommonStorageMetaInfo() meta = CommonStorageMetaInfo()
metaInfoProvider.set(meta) updateMetaInfo(meta)
} finally {
reader?.close()
} }
_metaInfo.value = meta _metaInfo.value = meta
} }
override suspend fun rename(newName: String) = withContext(ioDispatcher) { private suspend fun updateMetaInfo(meta: IStorageMetaInfo) = scope.run {
val cur = _metaInfo.value val writer = accessor.openWriteSystemFile(metaInfoFileName)
val newMeta = CommonStorageMetaInfo( try {
encInfo = cur.encInfo, jackson.writeValue(writer, meta)
name = newName } catch (e: Exception) {
) throw e
_metaInfo.value = newMeta } finally {
metaInfoProvider.set(newMeta) writer.close()
}
_metaInfo.value = meta
} }
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo) = withContext(ioDispatcher) { override suspend fun rename(newName: String) = scope.run {
val cur = _metaInfo.value val curMeta = metaInfo.value
val newMeta = CommonStorageMetaInfo( updateMetaInfo(
encInfo = encInfo, CommonStorageMetaInfo(
name = cur.name encInfo = curMeta.encInfo,
name = newName
)
)
}
override suspend fun setEncInfo(encInfo: StorageEncryptionInfo) = scope.run {
val curMeta = metaInfo.value
updateMetaInfo(
CommonStorageMetaInfo(
encInfo = encInfo,
name = curMeta.name
)
) )
_metaInfo.value = newMeta
metaInfoProvider.set(newMeta)
} }
override fun dispose() { override fun dispose() {
accessor.dispose() accessor.dispose()
job.cancel()
} }
companion object { companion object {
@@ -79,18 +122,25 @@ class EncryptedStorage private constructor(
source: IStorage, source: IStorage,
key: EncryptKey, key: EncryptKey,
ioDispatcher: CoroutineDispatcher, ioDispatcher: CoroutineDispatcher,
metaInfoProvider: StorageMetaInfoRepository.SingleStorageMetaInfoProvider,
uuid: UUID = UUID.randomUUID() uuid: UUID = UUID.randomUUID()
): EncryptedStorage = withContext(ioDispatcher) { ): EncryptedStorage = withContext(ioDispatcher) {
val storage = EncryptedStorage( val storage = EncryptedStorage(
source = source, source = source,
key = key, key = key,
ioDispatcher = ioDispatcher, ioDispatcher = ioDispatcher,
metaInfoProvider = metaInfoProvider,
uuid = uuid uuid = uuid
) )
storage.init() try {
storage.init()
} catch (e: Exception) {
storage.dispose()
throw e
}
return@withContext storage return@withContext storage
} }
private const val SYSTEM_HIDDEN_DIRNAME_POSTFIX = "-enc-dir"
const val STORAGE_INFO_FILE_POSTFIX = ".enc-meta"
private val jackson = jacksonObjectMapper().apply { findAndRegisterModules() }
} }
} }

View File

@@ -1,21 +1,23 @@
package com.github.nullptroma.wallenc.data.storages.encrypt package com.github.nullptroma.wallenc.data.storages.encrypt
import com.github.nullptroma.wallenc.data.utils.CloseHandledStreamExtension.Companion.onClosed
import com.github.nullptroma.wallenc.data.utils.CloseHandledStreamExtension.Companion.onClosing
import com.github.nullptroma.wallenc.domain.common.impl.CommonDirectory import com.github.nullptroma.wallenc.domain.common.impl.CommonDirectory
import com.github.nullptroma.wallenc.domain.common.impl.CommonFile import com.github.nullptroma.wallenc.domain.common.impl.CommonFile
import com.github.nullptroma.wallenc.domain.common.impl.CommonMetaInfo import com.github.nullptroma.wallenc.domain.common.impl.CommonMetaInfo
import com.github.nullptroma.wallenc.domain.datatypes.DataPackage import com.github.nullptroma.wallenc.domain.datatypes.DataPackage
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
import com.github.nullptroma.wallenc.domain.encrypt.Encryptor import com.github.nullptroma.wallenc.domain.encrypt.Encryptor
import com.github.nullptroma.wallenc.domain.encrypt.EncryptorWithStaticIv
import com.github.nullptroma.wallenc.domain.interfaces.IDirectory import com.github.nullptroma.wallenc.domain.interfaces.IDirectory
import com.github.nullptroma.wallenc.domain.interfaces.IFile import com.github.nullptroma.wallenc.domain.interfaces.IFile
import com.github.nullptroma.wallenc.domain.interfaces.IMetaInfo import com.github.nullptroma.wallenc.domain.interfaces.IMetaInfo
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -27,14 +29,17 @@ import kotlin.io.path.pathString
class EncryptedStorageAccessor( class EncryptedStorageAccessor(
private val source: IStorageAccessor, private val source: IStorageAccessor,
pathIv: ByteArray?,
key: EncryptKey, key: EncryptKey,
ioDispatcher: CoroutineDispatcher private val systemHiddenDirName: String,
private val scope: CoroutineScope
) : IStorageAccessor, DisposableHandle { ) : IStorageAccessor, DisposableHandle {
private val job = Job() private val _size = MutableStateFlow<Long?>(null)
private val scope = CoroutineScope(ioDispatcher + job) override val size: StateFlow<Long?> = _size
private val _numberOfFiles = MutableStateFlow<Int?>(null)
override val numberOfFiles: StateFlow<Int?> = _numberOfFiles
override val size: StateFlow<Long?> = source.size
override val numberOfFiles: StateFlow<Int?> = source.numberOfFiles
override val isAvailable: StateFlow<Boolean> = source.isAvailable override val isAvailable: StateFlow<Boolean> = source.isAvailable
private val _filesUpdates = MutableSharedFlow<DataPackage<List<IFile>>>() private val _filesUpdates = MutableSharedFlow<DataPackage<List<IFile>>>()
@@ -43,39 +48,70 @@ class EncryptedStorageAccessor(
private val _dirsUpdates = MutableSharedFlow<DataPackage<List<IDirectory>>>() private val _dirsUpdates = MutableSharedFlow<DataPackage<List<IDirectory>>>()
override val dirsUpdates: SharedFlow<DataPackage<List<IDirectory>>> = _dirsUpdates override val dirsUpdates: SharedFlow<DataPackage<List<IDirectory>>> = _dirsUpdates
private val encryptor = Encryptor(key.toAesKey()) private val dataEncryptor = Encryptor(key.toAesKey())
private val pathEncryptor: EncryptorWithStaticIv? = if(pathIv != null) EncryptorWithStaticIv(key.toAesKey(), pathIv) else null
private var systemHiddenFilesIsActual = false
init { init {
collectSourceState() collectSourceState()
} }
private fun collectSourceState() { private fun collectSourceState() {
scope.launch { scope.launch {
launch { launch {
source.filesUpdates.collect { source.filesUpdates.collect {
val files = it.data.map(::decryptEntity) val files = it.data.map(::decryptEntity).filterSystemHiddenFiles()
_filesUpdates.emit(DataPackage( _filesUpdates.emit(
data = files, DataPackage(
isLoading = it.isLoading, data = files,
isError = it.isError isLoading = it.isLoading,
)) isError = it.isError
)
)
} }
} }
launch { launch {
source.dirsUpdates.collect { source.dirsUpdates.collect {
val dirs = it.data.map(::decryptEntity) val dirs = it.data.map(::decryptEntity).filterSystemHiddenDirs()
_dirsUpdates.emit(DataPackage( _dirsUpdates.emit(
data = dirs, DataPackage(
isLoading = it.isLoading, data = dirs,
isError = it.isError isLoading = it.isLoading,
)) isError = it.isError
)
)
}
}
launch {
source.numberOfFiles.collect {
if(it == null)
_numberOfFiles.value = null
else
{
_numberOfFiles.value = it - getSystemFiles().size
}
}
}
launch {
source.size.collect { sourceSize ->
if(sourceSize == null)
_size.value = null
else
{
_size.value = sourceSize - getSystemFiles().sumOf { it.metaInfo.size }
}
} }
} }
} }
} }
private suspend fun getSystemFiles(): List<IFile> {
return source.getFiles(encryptPath(systemHiddenDirName))
}
private fun encryptEntity(file: IFile): IFile { private fun encryptEntity(file: IFile): IFile {
return CommonFile(encryptMeta(file.metaInfo)) return CommonFile(encryptMeta(file.metaInfo))
} }
@@ -113,35 +149,40 @@ class EncryptedStorageAccessor(
} }
private fun encryptPath(pathStr: String): String { private fun encryptPath(pathStr: String): String {
if(pathEncryptor == null)
return pathStr
val path = Path(pathStr) val path = Path(pathStr)
val segments = mutableListOf<String>() val segments = mutableListOf<String>()
for (segment in path) for (segment in path)
segments.add(encryptor.encryptString(segment.pathString)) segments.add(pathEncryptor.encryptString(segment.pathString))
val res = Path("/",*(segments.toTypedArray())) val res = Path("/", *(segments.toTypedArray()))
return res.pathString return res.pathString
} }
private fun decryptPath(pathStr: String): String { private fun decryptPath(pathStr: String): String {
if(pathEncryptor == null)
return pathStr
val path = Path(pathStr) val path = Path(pathStr)
val segments = mutableListOf<String>() val segments = mutableListOf<String>()
for (segment in path) for (segment in path)
segments.add(encryptor.decryptString(segment.pathString)) segments.add(pathEncryptor.decryptString(segment.pathString))
val res = Path("/",*(segments.toTypedArray())) val res = Path("/", *(segments.toTypedArray()))
return res.pathString return res.pathString
} }
override suspend fun getAllFiles(): List<IFile> { override suspend fun getAllFiles(): List<IFile> {
return source.getAllFiles().map(::decryptEntity) return source.getAllFiles().map(::decryptEntity).filterSystemHiddenFiles()
} }
override suspend fun getFiles(path: String): List<IFile> { override suspend fun getFiles(path: String): List<IFile> {
return source.getFiles(encryptPath(path)).map(::decryptEntity) return source.getFiles(encryptPath(path)).map(::decryptEntity).filterSystemHiddenFiles()
} }
override fun getFilesFlow(path: String): Flow<DataPackage<List<IFile>>> { override fun getFilesFlow(path: String): Flow<DataPackage<List<IFile>>> {
val flow = source.getFilesFlow(encryptPath(path)).map { val flow = source.getFilesFlow(encryptPath(path)).map {
DataPackage( DataPackage(
data = it.data.map(::decryptEntity), data = it.data.map(::decryptEntity).filterSystemHiddenFiles(),
isLoading = it.isLoading, isLoading = it.isLoading,
isError = it.isError isError = it.isError
) )
@@ -150,17 +191,18 @@ class EncryptedStorageAccessor(
} }
override suspend fun getAllDirs(): List<IDirectory> { override suspend fun getAllDirs(): List<IDirectory> {
return source.getAllDirs().map(::decryptEntity) return source.getAllDirs().map(::decryptEntity).filterSystemHiddenDirs()
} }
override suspend fun getDirs(path: String): List<IDirectory> { override suspend fun getDirs(path: String): List<IDirectory> {
return source.getDirs(encryptPath(path)).map(::decryptEntity) return source.getDirs(encryptPath(path)).map(::decryptEntity).filterSystemHiddenDirs()
} }
override fun getDirsFlow(path: String): Flow<DataPackage<List<IDirectory>>> { override fun getDirsFlow(path: String): Flow<DataPackage<List<IDirectory>>> {
val flow = source.getDirsFlow(encryptPath(path)).map { val flow = source.getDirsFlow(encryptPath(path)).map {
DataPackage( DataPackage(
data = it.data.map(::decryptEntity), // включать все папки, кроме системной
data = it.data.map(::decryptEntity).filterSystemHiddenDirs(),
isLoading = it.isLoading, isLoading = it.isLoading,
isError = it.isError isError = it.isError
) )
@@ -198,12 +240,12 @@ class EncryptedStorageAccessor(
override suspend fun openWrite(path: String): OutputStream { override suspend fun openWrite(path: String): OutputStream {
val stream = source.openWrite(encryptPath(path)) val stream = source.openWrite(encryptPath(path))
return encryptor.encryptStream(stream) return dataEncryptor.encryptStream(stream)
} }
override suspend fun openRead(path: String): InputStream { override suspend fun openRead(path: String): InputStream {
val stream = source.openRead(encryptPath(path)) val stream = source.openRead(encryptPath(path))
return encryptor.decryptStream(stream) return dataEncryptor.decryptStream(stream)
} }
override suspend fun moveToTrash(path: String) { override suspend fun moveToTrash(path: String) {
@@ -211,8 +253,36 @@ class EncryptedStorageAccessor(
} }
override fun dispose() { override fun dispose() {
job.cancel() dataEncryptor.dispose()
encryptor.dispose() }
suspend fun openReadSystemFile(name: String): InputStream = scope.run {
val path = Path(systemHiddenDirName, name).pathString
return@run openRead(path)
}
suspend fun openWriteSystemFile(name: String): OutputStream = scope.run {
val path = Path(systemHiddenDirName, name).pathString
systemHiddenFilesIsActual = false
return@run openWrite(path).onClosing {
systemHiddenFilesIsActual = false
}
}
private fun Iterable<IFile>.filterSystemHiddenFiles(): List<IFile> {
return this.filter { file ->
!file.metaInfo.path.contains(
systemHiddenDirName
)
}
}
private fun Iterable<IDirectory>.filterSystemHiddenDirs(): List<IDirectory> {
return this.filter { dir ->
!dir.metaInfo.path.contains(
systemHiddenDirName
)
}
} }
} }

View File

@@ -3,7 +3,7 @@ package com.github.nullptroma.wallenc.data.storages.local
import com.fasterxml.jackson.core.JacksonException import com.fasterxml.jackson.core.JacksonException
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.github.nullptroma.wallenc.data.utils.CloseHandledStreamExtension.Companion.onClose import com.github.nullptroma.wallenc.data.utils.CloseHandledStreamExtension.Companion.onClosed
import com.github.nullptroma.wallenc.domain.common.impl.CommonDirectory import com.github.nullptroma.wallenc.domain.common.impl.CommonDirectory
import com.github.nullptroma.wallenc.domain.common.impl.CommonFile import com.github.nullptroma.wallenc.domain.common.impl.CommonFile
import com.github.nullptroma.wallenc.domain.common.impl.CommonMetaInfo import com.github.nullptroma.wallenc.domain.common.impl.CommonMetaInfo
@@ -433,6 +433,8 @@ class LocalStorageAccessor(
if (file.exists() && file.isDirectory) { if (file.exists() && file.isDirectory) {
throw Exception("Что то пошло не так") // TODO throw Exception("Что то пошло не так") // TODO
} else if(!file.exists()) { } else if(!file.exists()) {
val parent = Path(storagePath).parent
createDir(parent.pathString)
file.createNewFile() file.createNewFile()
val cur = _numberOfFiles.value val cur = _numberOfFiles.value
@@ -441,7 +443,7 @@ class LocalStorageAccessor(
val pair = LocalStorageFilePair.from(_filesystemBasePath, file) val pair = LocalStorageFilePair.from(_filesystemBasePath, file)
?: throw Exception("Что то пошло не так") // TODO ?: throw Exception("Что то пошло не так") // TODO
val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant()) val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant(), size = Files.size(pair.file.toPath()))
writeMeta(pair.metaFile, newMeta) writeMeta(pair.metaFile, newMeta)
_filesUpdates.emit( _filesUpdates.emit(
DataPage( DataPage(
@@ -503,9 +505,10 @@ class LocalStorageAccessor(
touchFile(path) touchFile(path)
val pair = LocalStorageFilePair.from(_filesystemBasePath, path) val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
?: throw Exception("Файла нет") // TODO ?: throw Exception("Файла нет") // TODO
return@withContext pair.file.outputStream().onClose { return@withContext pair.file.outputStream().onClosed {
CoroutineScope(ioDispatcher).launch { CoroutineScope(ioDispatcher).launch {
touchFile(path) touchFile(path)
scanSizeAndNumOfFiles()
} }
} }
} }

View File

@@ -5,6 +5,7 @@ import java.io.OutputStream
private class CloseHandledOutputStream( private class CloseHandledOutputStream(
private val stream: OutputStream, private val stream: OutputStream,
private val onClosing: () -> Unit,
private val onClose: () -> Unit private val onClose: () -> Unit
) : OutputStream() { ) : OutputStream() {
@@ -25,6 +26,7 @@ private class CloseHandledOutputStream(
} }
override fun close() { override fun close() {
onClosing()
try { try {
stream.close() stream.close()
} finally { } finally {
@@ -35,6 +37,7 @@ private class CloseHandledOutputStream(
private class CloseHandledInputStream( private class CloseHandledInputStream(
private val stream: InputStream, private val stream: InputStream,
private val onClosing: () -> Unit,
private val onClose: () -> Unit private val onClose: () -> Unit
) : InputStream() { ) : InputStream() {
@@ -59,6 +62,7 @@ private class CloseHandledInputStream(
} }
override fun close() { override fun close() {
onClosing()
try { try {
stream.close() stream.close()
} finally { } finally {
@@ -81,12 +85,20 @@ private class CloseHandledInputStream(
class CloseHandledStreamExtension { class CloseHandledStreamExtension {
companion object { companion object {
fun OutputStream.onClose(callback: ()->Unit): OutputStream { fun OutputStream.onClosed(callback: ()->Unit): OutputStream {
return CloseHandledOutputStream(this, callback) return CloseHandledOutputStream(this, {}, callback)
} }
fun InputStream.onClose(callback: ()->Unit): InputStream { fun InputStream.onClosed(callback: ()->Unit): InputStream {
return CloseHandledInputStream(this, callback) return CloseHandledInputStream(this, {}, callback)
}
fun OutputStream.onClosing(callback: ()->Unit): OutputStream {
return CloseHandledOutputStream(this, callback, {})
}
fun InputStream.onClosing(callback: ()->Unit): InputStream {
return CloseHandledInputStream(this, callback, {})
} }
} }
} }

View File

@@ -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<Map<UUID, EncryptedStorage>?>(null)
override val openedStorages: StateFlow<Map<UUID, IStorage>?>
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<Boolean>
get() = MutableStateFlow(true)
override val totalSpace: StateFlow<Int?>
get() = MutableStateFlow(null)
override val availableSpace: StateFlow<Int?>
get() = MutableStateFlow(null)
override val storages: StateFlow<List<IStorage>?>
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<StorageKeyMap>()
val keysToRemove = mutableListOf<StorageKeyMap>()
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)
}
}

View File

@@ -1,19 +1,30 @@
package com.github.nullptroma.wallenc.data.vaults package com.github.nullptroma.wallenc.data.vaults
import android.content.Context import android.content.Context
import com.github.nullptroma.wallenc.data.db.app.repository.StorageKeyMapRepository
import com.github.nullptroma.wallenc.data.storages.UnlockManager
import com.github.nullptroma.wallenc.domain.interfaces.IStorage import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
import com.github.nullptroma.wallenc.domain.interfaces.IVault import com.github.nullptroma.wallenc.domain.interfaces.IVault
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
class VaultsManager(ioDispatcher: CoroutineDispatcher, context: Context) : IVaultsManager { class VaultsManager(ioDispatcher: CoroutineDispatcher, context: Context, keyRepo: StorageKeyMapRepository) : IVaultsManager {
override val localVault = LocalVault(ioDispatcher, context) override val localVault = LocalVault(ioDispatcher, context)
override val unlockManager: IUnlockManager = UnlockManager(
keymapRepository = keyRepo,
ioDispatcher = ioDispatcher,
vaultsManager = this
)
override val remoteVaults: StateFlow<List<IVault>> override val remoteVaults: StateFlow<List<IVault>>
get() = TODO("Not yet implemented") get() = TODO("Not yet implemented")
override val allStorages: StateFlow<List<IStorage>> override val allStorages: StateFlow<List<IStorage>>
get() = localVault.storages get() = localVault.storages
override val allVaults: StateFlow<List<IVault>>
get() = MutableStateFlow(listOf(localVault, unlockManager))
override fun addYandexVault(email: String, token: String) { override fun addYandexVault(email: String, token: String) {
TODO("Not yet implemented") TODO("Not yet implemented")

View File

@@ -9,10 +9,7 @@ import java.time.Instant
data class CommonStorageMetaInfo( data class CommonStorageMetaInfo(
override val encInfo: StorageEncryptionInfo = StorageEncryptionInfo( override val encInfo: StorageEncryptionInfo? = null,
isEncrypted = false,
encryptedTestData = null
),
override val name: String? = null, override val name: String? = null,
override val lastModified: Instant = Clock.systemUTC().instant() override val lastModified: Instant = Clock.systemUTC().instant()
) : IStorageMetaInfo ) : IStorageMetaInfo

View File

@@ -1,6 +1,24 @@
package com.github.nullptroma.wallenc.domain.datatypes package com.github.nullptroma.wallenc.domain.datatypes
data class StorageEncryptionInfo( data class StorageEncryptionInfo(
val isEncrypted: Boolean, val encryptedTestData: String,
val encryptedTestData: String? val pathIv: ByteArray?
) ) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as StorageEncryptionInfo
if (encryptedTestData != other.encryptedTestData) return false
if (!pathIv.contentEquals(other.pathIv)) return false
return true
}
override fun hashCode(): Int {
var result = encryptedTestData.hashCode()
result = 31 * result + pathIv.contentHashCode()
return result
}
}

View File

@@ -14,7 +14,7 @@ import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.random.Random import kotlin.random.Random
class Encryptor(private var _secretKey: SecretKey?) : DisposableHandle { class Encryptor(private var secretKey: SecretKey) : DisposableHandle {
@OptIn(ExperimentalEncodingApi::class) @OptIn(ExperimentalEncodingApi::class)
fun encryptString(str: String): String { fun encryptString(str: String): String {
val bytesToEncrypt = str.toByteArray(Charsets.UTF_8) val bytesToEncrypt = str.toByteArray(Charsets.UTF_8)
@@ -30,70 +30,71 @@ class Encryptor(private var _secretKey: SecretKey?) : DisposableHandle {
} }
fun encryptBytes(bytes: ByteArray): ByteArray { fun encryptBytes(bytes: ByteArray): ByteArray {
val secretKey = _secretKey ?: throw Exception("Object was disposed") if(secretKey.isDestroyed)
throw Exception("Object was destroyed")
val cipher = Cipher.getInstance(AES_SETTINGS) val cipher = Cipher.getInstance(AES_SETTINGS)
val iv = IvParameterSpec(Random.nextBytes(IV_LEN)) val ivSpec = IvParameterSpec(Random.nextBytes(IV_LEN))
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv) cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec)
val encryptedBytes = iv.iv + cipher.doFinal(bytes) // iv + зашифрованные байты val encryptedBytes = ivSpec.iv + cipher.doFinal(bytes) // iv + зашифрованные байты
return encryptedBytes return encryptedBytes
} }
fun decryptBytes(bytes: ByteArray): ByteArray { fun decryptBytes(bytes: ByteArray): ByteArray {
val secretKey = _secretKey ?: throw Exception("Object was disposed") if(secretKey.isDestroyed)
throw Exception("Object was destroyed")
val cipher = Cipher.getInstance(AES_SETTINGS) val cipher = Cipher.getInstance(AES_SETTINGS)
val iv = IvParameterSpec(bytes.take(IV_LEN).toByteArray()) val ivSpec = IvParameterSpec(bytes.take(IV_LEN).toByteArray())
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv) cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
val decryptedBytes = cipher.doFinal(bytes.drop(IV_LEN).toByteArray()) val decryptedBytes = cipher.doFinal(bytes.drop(IV_LEN).toByteArray())
return decryptedBytes return decryptedBytes
} }
fun encryptStream(stream: OutputStream): OutputStream { fun encryptStream(stream: OutputStream): OutputStream {
val secretKey = _secretKey ?: throw Exception("Object was disposed") if(secretKey.isDestroyed)
val iv = IvParameterSpec(Random.nextBytes(IV_LEN)) throw Exception("Object was destroyed")
stream.write(iv.iv) // Запись инициализационного вектора сырой файл val ivSpec = IvParameterSpec(Random.nextBytes(IV_LEN))
stream.write(ivSpec.iv) // Запись инициализационного вектора сырой файл
val cipher = Cipher.getInstance(AES_SETTINGS) val cipher = Cipher.getInstance(AES_SETTINGS)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv) // инициализация шифратора cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec) // инициализация шифратора
return CipherOutputStream(stream, cipher) return CipherOutputStream(stream, cipher)
} }
fun decryptStream(stream: InputStream): InputStream { fun decryptStream(stream: InputStream): InputStream {
val secretKey = _secretKey ?: throw Exception("Object was disposed") if(secretKey.isDestroyed)
throw Exception("Object was destroyed")
val ivBytes = ByteArray(IV_LEN) // Буфер для 16 байт IV val ivBytes = ByteArray(IV_LEN) // Буфер для 16 байт IV
val bytesRead = stream.read(ivBytes) // Чтение IV вектора val bytesRead = stream.read(ivBytes) // Чтение IV вектора
if(bytesRead != IV_LEN) if(bytesRead != IV_LEN)
throw Exception("TODO iv не прочитан") throw Exception("TODO iv не прочитан")
val iv = IvParameterSpec(ivBytes) val ivSpec = IvParameterSpec(ivBytes)
val cipher = Cipher.getInstance(AES_SETTINGS) val cipher = Cipher.getInstance(AES_SETTINGS)
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv) cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
return CipherInputStream(stream, cipher) return CipherInputStream(stream, cipher)
} }
override fun dispose() { override fun dispose() {
_secretKey?.destroy() //secretKey.destroy()
_secretKey = null
} }
companion object { companion object {
private const val IV_LEN = 16 public const val IV_LEN = 16
public const val AES_SETTINGS = "AES/CBC/PKCS5Padding"
private const val TEST_DATA_LEN = 512 private const val TEST_DATA_LEN = 512
private const val AES_SETTINGS = "AES/CBC/PKCS5Padding"
@OptIn(ExperimentalEncodingApi::class) @OptIn(ExperimentalEncodingApi::class)
fun generateEncryptionInfo(key: EncryptKey) : StorageEncryptionInfo { fun generateEncryptionInfo(key: EncryptKey, encryptPath: Boolean = true) : StorageEncryptionInfo {
val encryptor = Encryptor(key.toAesKey()) val encryptor = Encryptor(key.toAesKey())
val testData = ByteArray(TEST_DATA_LEN) val testData = ByteArray(TEST_DATA_LEN)
val encryptedData = encryptor.encryptBytes(testData) val encryptedData = encryptor.encryptBytes(testData)
return StorageEncryptionInfo( return StorageEncryptionInfo(
isEncrypted = true, encryptedTestData = Base64.Default.encode(encryptedData),
encryptedTestData = Base64.Default.encode(encryptedData) pathIv = if(encryptPath) Random.nextBytes(IV_LEN) else null
) )
} }
@OptIn(ExperimentalEncodingApi::class) @OptIn(ExperimentalEncodingApi::class)
fun checkKey(key: EncryptKey, encInfo: StorageEncryptionInfo): Boolean { fun checkKey(key: EncryptKey, encInfo: StorageEncryptionInfo): Boolean {
if(encInfo.encryptedTestData == null)
return false
val encryptor = Encryptor(key.toAesKey()) val encryptor = Encryptor(key.toAesKey())
try { try {
val encData = Base64.Default.decode(encInfo.encryptedTestData) val encData = Base64.Default.decode(encInfo.encryptedTestData)

View File

@@ -0,0 +1,73 @@
package com.github.nullptroma.wallenc.domain.encrypt
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
import com.github.nullptroma.wallenc.domain.encrypt.Encryptor.Companion.AES_SETTINGS
import com.github.nullptroma.wallenc.domain.encrypt.Encryptor.Companion.IV_LEN
import kotlinx.coroutines.DisposableHandle
import java.io.InputStream
import java.io.OutputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.random.Random
class EncryptorWithStaticIv(private var secretKey: SecretKey, iv: ByteArray) : DisposableHandle {
private val ivSpec = IvParameterSpec(iv)
@OptIn(ExperimentalEncodingApi::class)
fun encryptString(str: String): String {
val bytesToEncrypt = str.toByteArray(Charsets.UTF_8)
val encryptedBytes = encryptBytes(bytesToEncrypt)
return Base64.Default.encode(encryptedBytes).replace("/", ".")
}
@OptIn(ExperimentalEncodingApi::class)
fun decryptString(str: String): String {
val bytesToDecrypt = Base64.Default.decode(str.replace(".", "/"))
val decryptedBytes = decryptBytes(bytesToDecrypt)
return String(decryptedBytes, Charsets.UTF_8)
}
fun encryptBytes(bytes: ByteArray): ByteArray {
if(secretKey.isDestroyed)
throw Exception("Object was destroyed")
val cipher = Cipher.getInstance(AES_SETTINGS)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec)
val encryptedBytes = cipher.doFinal(bytes) // зашифрованные байты
return encryptedBytes
}
fun decryptBytes(bytes: ByteArray): ByteArray {
if(secretKey.isDestroyed)
throw Exception("Object was destroyed")
val cipher = Cipher.getInstance(AES_SETTINGS)
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
val decryptedBytes = cipher.doFinal(bytes)
return decryptedBytes
}
fun encryptStream(stream: OutputStream): OutputStream {
if(secretKey.isDestroyed)
throw Exception("Object was destroyed")
val cipher = Cipher.getInstance(AES_SETTINGS)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec) // инициализация шифратора
return CipherOutputStream(stream, cipher)
}
fun decryptStream(stream: InputStream): InputStream {
if(secretKey.isDestroyed)
throw Exception("Object was destroyed")
val cipher = Cipher.getInstance(AES_SETTINGS)
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
return CipherInputStream(stream, cipher)
}
override fun dispose() {
secretKey.destroy()
}
}

View File

@@ -2,5 +2,6 @@ package com.github.nullptroma.wallenc.domain.enums
enum class VaultType { enum class VaultType {
LOCAL, LOCAL,
DECRYPTED,
YANDEX YANDEX
} }

View File

@@ -1,6 +1,18 @@
package com.github.nullptroma.wallenc.domain.interfaces package com.github.nullptroma.wallenc.domain.interfaces
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
import kotlinx.coroutines.flow.StateFlow
import java.time.Instant
import java.util.UUID
sealed interface IStorageInfo {
val uuid: UUID
val isAvailable: StateFlow<Boolean>
val size: StateFlow<Long?>
val numberOfFiles: StateFlow<Int?>
val metaInfo: StateFlow<IStorageMetaInfo>
val isVirtualStorage: Boolean
}
interface IStorage: IStorageInfo { interface IStorage: IStorageInfo {
val accessor: IStorageAccessor val accessor: IStorageAccessor
@@ -8,3 +20,9 @@ interface IStorage: IStorageInfo {
suspend fun rename(newName: String) suspend fun rename(newName: String)
suspend fun setEncInfo(encInfo: StorageEncryptionInfo) suspend fun setEncInfo(encInfo: StorageEncryptionInfo)
} }
interface IStorageMetaInfo {
val encInfo: StorageEncryptionInfo?
val name: String?
val lastModified: Instant
}

View File

@@ -1,13 +0,0 @@
package com.github.nullptroma.wallenc.domain.interfaces
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
sealed interface IStorageInfo {
val uuid: UUID
val isAvailable: StateFlow<Boolean>
val size: StateFlow<Long?>
val numberOfFiles: StateFlow<Int?>
val metaInfo: StateFlow<IStorageMetaInfo>
val isVirtualStorage: Boolean
}

View File

@@ -1,11 +0,0 @@
package com.github.nullptroma.wallenc.domain.interfaces
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
import java.time.Instant
interface IStorageMetaInfo {
val encInfo: StorageEncryptionInfo
val name: String?
val lastModified: Instant
}

View File

@@ -4,12 +4,13 @@ import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import java.util.UUID import java.util.UUID
interface IUnlockManager { interface IUnlockManager: IVault {
/** /**
* Хранилища, для которых есть ключ шифрования * Хранилища, для которых есть ключ шифрования
*/ */
val openedStorages: StateFlow<Map<UUID, IStorage>?> val openedStorages: StateFlow<Map<UUID, IStorage>?>
suspend fun open(storage: IStorage, key: EncryptKey) suspend fun open(storage: IStorage, key: EncryptKey): IStorage
suspend fun close(storage: IStorage) suspend fun close(storage: IStorage)
suspend fun close(uuid: UUID): Unit
} }

View File

@@ -4,7 +4,7 @@ import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface IVault : IVaultInfo { interface IVault : IVaultInfo {
override val storages: StateFlow<List<IStorage>> override val storages: StateFlow<List<IStorage>?>
suspend fun createStorage(): IStorage suspend fun createStorage(): IStorage
suspend fun createStorage(enc: StorageEncryptionInfo): IStorage suspend fun createStorage(enc: StorageEncryptionInfo): IStorage

View File

@@ -7,7 +7,7 @@ import java.util.UUID
sealed interface IVaultInfo { sealed interface IVaultInfo {
val type: VaultType val type: VaultType
val uuid: UUID val uuid: UUID
val storages: StateFlow<List<IStorageInfo>> val storages: StateFlow<List<IStorageInfo>?>
val isAvailable: StateFlow<Boolean> val isAvailable: StateFlow<Boolean>
val totalSpace: StateFlow<Int?> val totalSpace: StateFlow<Int?>
val availableSpace: StateFlow<Int?> val availableSpace: StateFlow<Int?>

View File

@@ -4,8 +4,9 @@ import kotlinx.coroutines.flow.StateFlow
interface IVaultsManager { interface IVaultsManager {
val localVault: IVault val localVault: IVault
val unlockManager: IUnlockManager
val remoteVaults: StateFlow<List<IVault>> val remoteVaults: StateFlow<List<IVault>>
val allStorages: StateFlow<List<IStorage>> val allStorages: StateFlow<List<IStorage>>
val allVaults: StateFlow<List<IVault>>
fun addYandexVault(email: String, token: String) fun addYandexVault(email: String, token: String)
} }

View File

@@ -10,19 +10,13 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class ManageLocalVaultUseCase(private val manager: IVaultsManager, private val unlockManager: IUnlockManager) { class ManageLocalVaultUseCase(private val manager: IVaultsManager, private val unlockManager: IUnlockManager) {
val localStorages: StateFlow<List<IStorageInfo>> val localStorages: StateFlow<List<IStorageInfo>?>
get() = manager.localVault.storages get() = manager.localVault.storages
suspend fun createStorage() { suspend fun createStorage() {
manager.localVault.createStorage() manager.localVault.createStorage()
} }
suspend fun createStorage(key: EncryptKey) {
val encInfo = Encryptor.generateEncryptionInfo(key)
val storage = manager.localVault.createStorage(encInfo)
unlockManager.open(storage, key)
}
suspend fun remove(storage: IStorageInfo) { suspend fun remove(storage: IStorageInfo) {
when(storage) { when(storage) {
is IStorage -> manager.localVault.remove(storage) is IStorage -> manager.localVault.remove(storage)

View File

@@ -0,0 +1,27 @@
package com.github.nullptroma.wallenc.domain.usecases
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
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
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))
}
}
}
suspend fun openStorage(storage: IStorageInfo, key: EncryptKey): IStorageInfo {
when(storage) {
is IStorage -> {
return unlockManager.open(storage, key)
}
}
}
}

View File

@@ -0,0 +1,12 @@
package com.github.nullptroma.wallenc.domain.usecases
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
class RemoveStorageUseCase {
suspend fun rename(storage: IStorageInfo, newName: String) {
when (storage) {
is IStorage -> storage.rename(newName)
}
}
}

View File

@@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.8.0" agp = "8.9.1"
jacksonModuleKotlin = "2.18.2" jacksonModuleKotlin = "2.18.2"
kotlin = "2.0.20" kotlin = "2.0.20"
coreKtx = "1.15.0" coreKtx = "1.15.0"

View File

@@ -1,6 +1,6 @@
#Sat Sep 07 01:04:14 MSK 2024 #Sat Sep 07 01:04:14 MSK 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -3,8 +3,11 @@ package com.github.nullptroma.wallenc.presentation
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Menu import androidx.compose.material.icons.rounded.Menu
import androidx.compose.material.icons.rounded.Settings 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.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@@ -71,13 +75,15 @@ fun WallencNavRoot(viewModel: WallencViewModel = hiltViewModel()) {
Scaffold(bottomBar = { Scaffold(bottomBar = {
NavigationBar(modifier = Modifier.height(64.dp)) { NavigationBar(modifier = Modifier.wrapContentHeight()) {
val navBackStackEntry by navState.navHostController.currentBackStackEntryAsState() val navBackStackEntry by navState.navHostController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route val currentRoute = navBackStackEntry?.destination?.route
topLevelNavBarItems.forEach { topLevelNavBarItems.forEach {
val routeClassName = it.key val routeClassName = it.key
val navBarItemData = it.value val navBarItemData = it.value
NavigationBarItem(icon = { NavigationBarItem(
modifier = Modifier.wrapContentHeight(),
icon = {
if (navBarItemData.icon != null) Icon( if (navBarItemData.icon != null) Icon(
navBarItemData.icon, navBarItemData.icon,
contentDescription = stringResource(navBarItemData.nameStringResourceId) contentDescription = stringResource(navBarItemData.nameStringResourceId)
@@ -92,7 +98,8 @@ fun WallencNavRoot(viewModel: WallencViewModel = hiltViewModel()) {
if (currentRoute?.startsWith(routeClassName) != true) navState.changeTop( if (currentRoute?.startsWith(routeClassName) != true) navState.changeTop(
route route
) )
}) }
)
} }
} }
}) { innerPaddings -> }) { innerPaddings ->

View File

@@ -1,17 +1,25 @@
package com.github.nullptroma.wallenc.presentation.elements package com.github.nullptroma.wallenc.presentation.elements
import android.widget.FrameLayout
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
@@ -20,7 +28,9 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -28,16 +38,20 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.PlatformTextStyle import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.domain.datatypes.Tree import com.github.nullptroma.wallenc.domain.datatypes.Tree
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
import com.github.nullptroma.wallenc.presentation.R import com.github.nullptroma.wallenc.presentation.R
import com.github.nullptroma.wallenc.presentation.utils.debouncedLambda
@Composable @Composable
fun StorageTree( fun StorageTree(
@@ -46,118 +60,169 @@ fun StorageTree(
onClick: (Tree<IStorageInfo>) -> Unit, onClick: (Tree<IStorageInfo>) -> Unit,
onRename: (Tree<IStorageInfo>, String) -> Unit, onRename: (Tree<IStorageInfo>, String) -> Unit,
onRemove: (Tree<IStorageInfo>) -> Unit, onRemove: (Tree<IStorageInfo>) -> Unit,
onEncrypt: (Tree<IStorageInfo>) -> Unit,
) { ) {
val cur = tree.value val cur = tree.value
val cardShape = RoundedCornerShape(30.dp) val available by cur.isAvailable.collectAsStateWithLifecycle()
val numOfFiles by cur.numberOfFiles.collectAsStateWithLifecycle()
val size by cur.size.collectAsStateWithLifecycle()
val metaInfo by cur.metaInfo.collectAsStateWithLifecycle()
val isAvailable by cur.isAvailable.collectAsStateWithLifecycle()
val borderColor =
if (cur.isVirtualStorage) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.primary
Column(modifier) { Column(modifier) {
Card( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .height(IntrinsicSize.Min)
.clip(cardShape) .zIndex(100f)
.clickable {
onClick(tree)
//viewModel.printStorageInfoToLog(cur)
},
shape = cardShape,
elevation = CardDefaults.cardElevation(
defaultElevation = 4.dp
),
) { ) {
val available by cur.isAvailable.collectAsStateWithLifecycle() val interactionSource = remember { MutableInteractionSource() }
val numOfFiles by cur.numberOfFiles.collectAsStateWithLifecycle() Box(
val size by cur.size.collectAsStateWithLifecycle() modifier = Modifier
val metaInfo by cur.metaInfo.collectAsStateWithLifecycle() .clip(
CardDefaults.shape
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
Column(modifier = Modifier.padding(8.dp)) {
Text(metaInfo.name ?: stringResource(R.string.no_name))
Text(
text = "IsAvailable: $available"
) )
Text("Files: $numOfFiles") .padding(0.dp, 0.dp, 16.dp, 0.dp)
Text("Size: $size") .fillMaxSize()
Text("IsVirtual: ${cur.isVirtualStorage}") .background(borderColor)
.clickable(
interactionSource = interactionSource,
indication = ripple(),
enabled = false,
onClick = { }
)
)
Card(
interactionSource = interactionSource,
modifier = Modifier
.padding(8.dp, 0.dp, 0.dp, 0.dp)
.fillMaxWidth(),
elevation = CardDefaults.cardElevation(
defaultElevation = 4.dp
),
onClick = debouncedLambda(debounceMs = 500) {
onClick(tree)
} }
Column( ) {
modifier = Modifier,
horizontalAlignment = Alignment.End
) {
Box(modifier = Modifier.padding(0.dp, 8.dp, 8.dp, 0.dp)) {
var expanded by remember { mutableStateOf(false) }
var showRenameDialog by remember { mutableStateOf(false) }
var showRemoveConfirmationDiaglog by remember { mutableStateOf(false) }
IconButton(onClick = { expanded = !expanded }) {
Icon(
Icons.Default.MoreVert,
contentDescription = stringResource(R.string.show_storage_item_menu)
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
onClick = {
expanded = false
showRenameDialog = true
},
text = { Text(stringResource(R.string.rename)) }
)
HorizontalDivider()
DropdownMenuItem(
onClick = {
expanded = false
showRemoveConfirmationDiaglog = true;
},
text = { Text(stringResource(R.string.remove)) }
)
}
if (showRenameDialog) {
TextEditCancelOkDialog(
onDismiss = { showRenameDialog = false },
onConfirmation = { newName ->
showRenameDialog = false
onRename(tree, newName)
},
title = stringResource(R.string.new_name_title),
startString = metaInfo.name ?: ""
)
}
if (showRemoveConfirmationDiaglog) { Row(modifier = Modifier.height(IntrinsicSize.Min)) {
ConfirmationCancelOkDialog( Column(modifier = Modifier.padding(8.dp)) {
onDismiss = { Text(metaInfo.name ?: stringResource(R.string.no_name))
showRemoveConfirmationDiaglog = false Text(
}, text = "IsAvailable: $available"
onConfirmation = { )
showRemoveConfirmationDiaglog = false Text("Files: $numOfFiles")
onRemove(tree) Text("Size: $size")
}, Text("IsVirtual: ${cur.isVirtualStorage}")
title = stringResource(R.string.remove_confirmation_dialog, metaInfo.name ?: "<noname>")
)
}
} }
Spacer(modifier = Modifier.weight(1f)) Column(
Text( modifier = Modifier,
modifier = Modifier horizontalAlignment = Alignment.End
.fillMaxWidth() ) {
.padding(0.dp, 0.dp, 12.dp, 8.dp) Box(modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 0.dp)) {
.align(Alignment.End), var expanded by remember { mutableStateOf(false) }
text = cur.uuid.toString(), var showRenameDialog by remember { mutableStateOf(false) }
textAlign = TextAlign.End, var showRemoveConfirmationDiaglog by remember { mutableStateOf(false) }
fontSize = 8.sp, IconButton(onClick = { expanded = !expanded }) {
style = LocalTextStyle.current.copy( Icon(
platformStyle = PlatformTextStyle( Icons.Default.MoreVert,
includeFontPadding = true contentDescription = stringResource(R.string.show_storage_item_menu)
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
onClick = {
expanded = false
showRenameDialog = true
},
text = { Text(stringResource(R.string.rename)) }
)
HorizontalDivider()
DropdownMenuItem(
onClick = {
expanded = false
showRemoveConfirmationDiaglog = true;
},
text = { Text(stringResource(R.string.remove)) }
)
}
if (showRenameDialog) {
TextEditCancelOkDialog(
onDismiss = { showRenameDialog = false },
onConfirmation = { newName ->
showRenameDialog = false
onRename(tree, newName)
},
title = stringResource(R.string.new_name_title),
startString = metaInfo.name ?: ""
)
}
if (showRemoveConfirmationDiaglog) {
ConfirmationCancelOkDialog(
onDismiss = {
showRemoveConfirmationDiaglog = false
},
onConfirmation = {
showRemoveConfirmationDiaglog = false
onRemove(tree)
},
title = stringResource(
R.string.remove_confirmation_dialog,
metaInfo.name ?: "<noname>"
)
)
}
}
Spacer(modifier = Modifier.weight(1f))
Button(onClick = { onEncrypt(tree) }, enabled = metaInfo.encInfo == null) {
Text("Encrypt")
}
Text(
modifier = Modifier
.fillMaxWidth()
.padding(0.dp, 0.dp, 12.dp, 8.dp)
.align(Alignment.End),
text = cur.uuid.toString(),
textAlign = TextAlign.End,
fontSize = 8.sp,
style = LocalTextStyle.current.copy(
platformStyle = PlatformTextStyle(
includeFontPadding = true
)
) )
) )
) }
} }
} }
if(!isAvailable) {
Box(
modifier = Modifier
.clip(
CardDefaults.shape
)
.fillMaxSize()
.alpha(0.5f)
.background(Color.Black)
)
}
} }
for (i in tree.children ?: listOf()) { for (i in tree.children ?: listOf()) {
StorageTree(Modifier.padding(16.dp, 0.dp, 0.dp, 0.dp), i, onClick, onRename, onRemove) StorageTree(
Modifier
.padding(16.dp, 0.dp, 0.dp, 0.dp)
.offset(y = (-4).dp),
i,
onClick,
onRename,
onRemove,
onEncrypt
)
} }
} }
} }

View File

@@ -0,0 +1,61 @@
package com.github.nullptroma.wallenc.presentation.elements.indication
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.spring
import androidx.compose.foundation.IndicationNodeFactory
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.DrawModifierNode
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
private class ScaleNode(private val interactionSource: InteractionSource) :
Modifier.Node(), DrawModifierNode {
var currentPressPosition: Offset = Offset.Zero
val animatedScalePercent = Animatable(1f)
private suspend fun animateToPressed(pressPosition: Offset) {
currentPressPosition = pressPosition
animatedScalePercent.animateTo(0.9f, spring())
}
private suspend fun animateToResting() {
animatedScalePercent.animateTo(1f, spring())
}
override fun onAttach() {
coroutineScope.launch {
interactionSource.interactions.collectLatest { interaction ->
when (interaction) {
is PressInteraction.Press -> animateToPressed(interaction.pressPosition)
is PressInteraction.Release -> animateToResting()
is PressInteraction.Cancel -> animateToResting()
}
}
}
}
override fun ContentDrawScope.draw() {
scale(
scale = animatedScalePercent.value,
pivot = currentPressPosition
) {
this@draw.drawContent()
}
}
}
object ScaleIndication : IndicationNodeFactory {
override fun create(interactionSource: InteractionSource): DelegatableNode {
return ScaleNode(interactionSource)
}
override fun equals(other: Any?): Boolean = other === ScaleIndication
override fun hashCode() = 100
}

View File

@@ -1,7 +1,19 @@
package com.github.nullptroma.wallenc.presentation.extensions package com.github.nullptroma.wallenc.presentation.extensions
import androidx.compose.foundation.Indication
import androidx.compose.foundation.IndicationNodeFactory
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
fun Modifier.ignoreHorizontalParentPadding(horizontal: Dp): Modifier { fun Modifier.ignoreHorizontalParentPadding(horizontal: Dp): Modifier {
@@ -23,3 +35,19 @@ fun Modifier.ignoreVerticalParentPadding(vertical: Dp): Modifier {
} }
} }
} }
fun Modifier.gesturesDisabled(disabled: Boolean = true) =
if (disabled) {
pointerInput(Unit) {
awaitPointerEventScope {
// we should wait for all new pointer events
while (true) {
awaitPointerEvent(pass = PointerEventPass.Initial)
.changes
.forEach(PointerInputChange::consume)
}
}
}
} else {
this
}

View File

@@ -3,6 +3,7 @@ package com.github.nullptroma.wallenc.presentation.screens.main
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@@ -17,6 +18,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@@ -62,15 +64,14 @@ fun MainScreen(
Scaffold(modifier = modifier, contentWindowInsets = WindowInsets(0.dp), bottomBar = { Scaffold(modifier = modifier, contentWindowInsets = WindowInsets(0.dp), bottomBar = {
Column { Column {
NavigationBar(modifier = Modifier.height(48.dp)) { NavigationBar(windowInsets = WindowInsets(0), modifier = Modifier.height(48.dp)) {
val navBackStackEntry by navState.navHostController.currentBackStackEntryAsState() val navBackStackEntry by navState.navHostController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route val currentRoute = navBackStackEntry?.destination?.route
topLevelNavBarItems.forEach { topLevelNavBarItems.forEach {
val routeClassName = it.key val routeClassName = it.key
val navBarItemData = it.value val navBarItemData = it.value
NavigationBarItem(modifier = Modifier NavigationBarItem(modifier = Modifier
.weight(1f) .weight(1f),
.fillMaxHeight(),
icon = { Text(stringResource(navBarItemData.nameStringResourceId)) }, icon = { Text(stringResource(navBarItemData.nameStringResourceId)) },
selected = currentRoute?.startsWith(routeClassName) == true, selected = currentRoute?.startsWith(routeClassName) == true,
onClick = { onClick = {
@@ -80,7 +81,9 @@ fun MainScreen(
navState.changeTop( navState.changeTop(
route route
) )
}) },
label = null
)
} }
} }
HorizontalDivider() HorizontalDivider()

View File

@@ -1,56 +1,39 @@
package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault
import android.widget.ProgressBar
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Card import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CardElevation
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.github.nullptroma.wallenc.domain.datatypes.Tree
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
import com.github.nullptroma.wallenc.presentation.R
import com.github.nullptroma.wallenc.presentation.elements.StorageTree import com.github.nullptroma.wallenc.presentation.elements.StorageTree
import kotlin.random.Random import com.github.nullptroma.wallenc.presentation.extensions.gesturesDisabled
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LocalVaultScreen( fun LocalVaultScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@@ -59,29 +42,48 @@ fun LocalVaultScreen(
) { ) {
val uiState by viewModel.state.collectAsStateWithLifecycle() val uiState by viewModel.state.collectAsStateWithLifecycle()
Scaffold(modifier = modifier, contentWindowInsets = WindowInsets(0.dp), floatingActionButton = { Box {
FloatingActionButton( Scaffold(modifier = modifier, contentWindowInsets = WindowInsets(0.dp), floatingActionButton = {
onClick = { FloatingActionButton(
viewModel.createStorage() onClick = {
}, viewModel.createStorage()
) { },
Icon(Icons.Filled.Add, "Floating action button.") ) {
Icon(Icons.Filled.Add, "Floating action button.")
}
}) { innerPadding ->
LazyColumn(modifier = Modifier.padding(innerPadding).gesturesDisabled(uiState.isLoading)) {
items(uiState.storagesList) { listItem ->
StorageTree(
modifier = Modifier.padding(8.dp, 8.dp, 8.dp, 0.dp),
tree = listItem,
onClick = {
openTextEdit(it.value.uuid.toString())
},
onRename = { tree, newName ->
viewModel.rename(tree.value, newName)
},
onRemove = { tree ->
viewModel.remove(tree.value)
},
onEncrypt = { tree ->
viewModel.enableEncryptionAndOpenStorage(tree.value)
}
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
}
}
} }
}) { innerPadding ->
LazyColumn(modifier = Modifier.padding(innerPadding)) { if(uiState.isLoading) {
items(uiState.storagesList) { listItem -> Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
StorageTree( Box(modifier = Modifier.fillMaxSize().alpha(0.6f).background(Color.Black))
modifier = Modifier.padding(8.dp), CircularProgressIndicator(
tree = listItem, modifier = Modifier.width(64.dp),
onClick = { color = MaterialTheme.colorScheme.secondary,
openTextEdit(it.value.uuid.toString()) trackColor = MaterialTheme.colorScheme.surfaceVariant,
},
onRename = { tree, newName ->
viewModel.rename(tree.value, newName)
},
onRemove = { tree ->
viewModel.remove(tree.value)
}
) )
} }
} }

View File

@@ -3,4 +3,4 @@ package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.va
import com.github.nullptroma.wallenc.domain.datatypes.Tree import com.github.nullptroma.wallenc.domain.datatypes.Tree
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
data class LocalVaultScreenState(val storagesList: List<Tree<IStorageInfo>>) data class LocalVaultScreenState(val storagesList: List<Tree<IStorageInfo>>, val isLoading: Boolean)

View File

@@ -1,6 +1,7 @@
package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
import com.github.nullptroma.wallenc.domain.datatypes.Tree import com.github.nullptroma.wallenc.domain.datatypes.Tree
import com.github.nullptroma.wallenc.domain.interfaces.IDirectory import com.github.nullptroma.wallenc.domain.interfaces.IDirectory
import com.github.nullptroma.wallenc.domain.interfaces.IFile import com.github.nullptroma.wallenc.domain.interfaces.IFile
@@ -8,10 +9,11 @@ import com.github.nullptroma.wallenc.domain.interfaces.ILogger
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
import com.github.nullptroma.wallenc.domain.usecases.GetOpenedStoragesUseCase import com.github.nullptroma.wallenc.domain.usecases.GetOpenedStoragesUseCase
import com.github.nullptroma.wallenc.domain.usecases.ManageLocalVaultUseCase import com.github.nullptroma.wallenc.domain.usecases.ManageLocalVaultUseCase
import com.github.nullptroma.wallenc.domain.usecases.ManageStoragesEncryptionUseCase
import com.github.nullptroma.wallenc.domain.usecases.RenameStorageUseCase import com.github.nullptroma.wallenc.domain.usecases.RenameStorageUseCase
import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase
import com.github.nullptroma.wallenc.presentation.extensions.toPrintable
import com.github.nullptroma.wallenc.presentation.ViewModelBase import com.github.nullptroma.wallenc.presentation.ViewModelBase
import com.github.nullptroma.wallenc.presentation.extensions.toPrintable
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@@ -24,17 +26,46 @@ class LocalVaultViewModel @Inject constructor(
private val manageLocalVaultUseCase: ManageLocalVaultUseCase, private val manageLocalVaultUseCase: ManageLocalVaultUseCase,
private val getOpenedStoragesUseCase: GetOpenedStoragesUseCase, private val getOpenedStoragesUseCase: GetOpenedStoragesUseCase,
private val storageFileManagementUseCase: StorageFileManagementUseCase, private val storageFileManagementUseCase: StorageFileManagementUseCase,
private val manageStoragesEncryptionUseCase: ManageStoragesEncryptionUseCase,
private val renameStorageUseCase: RenameStorageUseCase, private val renameStorageUseCase: RenameStorageUseCase,
private val logger: ILogger private val logger: ILogger
) : ViewModelBase<LocalVaultScreenState>(LocalVaultScreenState(listOf())) { ) : ViewModelBase<LocalVaultScreenState>(LocalVaultScreenState(listOf(), true)) {
private var _taskCount: Int = 0
private var tasksCount
get() = _taskCount
set(value) {
_taskCount = value
updateStateLoading()
}
private var _isLoading: Boolean = false
private var isLoading
get() = _isLoading
set(value) {
_isLoading = value
updateStateLoading()
}
init { init {
collectFlows()
}
private fun updateStateLoading() {
updateState(state.value.copy(
isLoading = this.isLoading || this.tasksCount > 0
))
}
private fun collectFlows() {
viewModelScope.launch { viewModelScope.launch {
manageLocalVaultUseCase.localStorages.combine(getOpenedStoragesUseCase.openedStorages) { local, opened -> manageLocalVaultUseCase.localStorages.combine(getOpenedStoragesUseCase.openedStorages) { local, opened ->
if(local == null || opened == null)
return@combine null
val list = mutableListOf<Tree<IStorageInfo>>() val list = mutableListOf<Tree<IStorageInfo>>()
for (storage in local) { for (storage in local) {
var tree = Tree(storage) var tree = Tree(storage)
list.add(tree) list.add(tree)
while(opened != null && opened.containsKey(tree.value.uuid)) { while(opened.containsKey(tree.value.uuid)) {
val child = opened.getValue(tree.value.uuid) val child = opened.getValue(tree.value.uuid)
val nextTree = Tree(child) val nextTree = Tree(child)
tree.children = listOf(nextTree) tree.children = listOf(nextTree)
@@ -43,8 +74,9 @@ class LocalVaultViewModel @Inject constructor(
} }
return@combine list return@combine list
}.collectLatest { }.collectLatest {
isLoading = it == null
val newState = state.value.copy( val newState = state.value.copy(
storagesList = it storagesList = it ?: listOf()
) )
updateState(newState) updateState(newState)
} }
@@ -72,8 +104,29 @@ class LocalVaultViewModel @Inject constructor(
} }
fun createStorage() { fun createStorage() {
tasksCount++
viewModelScope.launch { viewModelScope.launch {
manageLocalVaultUseCase.createStorage() manageLocalVaultUseCase.createStorage()
tasksCount--
}
}
private val runningStorages = mutableSetOf<IStorageInfo>()
fun enableEncryptionAndOpenStorage(storage: IStorageInfo) {
if(runningStorages.contains(storage))
return
tasksCount++
runningStorages.add(storage)
val key = EncryptKey("Hello")
viewModelScope.launch {
try {
manageStoragesEncryptionUseCase.enableEncryption(storage, key, false)
manageStoragesEncryptionUseCase.openStorage(storage, key)
}
finally {
runningStorages.remove(storage)
tasksCount--
}
} }
} }

View File

@@ -0,0 +1,13 @@
package com.github.nullptroma.wallenc.presentation.utils
fun debouncedLambda(debounceMs: Long = 300, action: ()->Unit) : ()->Unit {
var latest: Long = 0
return {
val now = System.currentTimeMillis()
if (now - latest >= debounceMs) {
latest = now
action()
}
}
}