Compare commits
10 Commits
4615b04fdf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce99888c6e | ||
|
|
824306d8bc | ||
|
|
85b8517a76 | ||
|
|
e1646611c2 | ||
|
|
4404ef2ff4 | ||
|
|
86b5c6cae2 | ||
|
|
da8808a4b9 | ||
|
|
c95c374852 | ||
|
|
2cb2dabe3f | ||
|
|
f7071382a7 |
Binary file not shown.
Binary file not shown.
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,5 +2,6 @@ package com.github.nullptroma.wallenc.domain.enums
|
|||||||
|
|
||||||
enum class VaultType {
|
enum class VaultType {
|
||||||
LOCAL,
|
LOCAL,
|
||||||
|
DECRYPTED,
|
||||||
YANDEX
|
YANDEX
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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?>
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||||
|
|||||||
@@ -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 ->
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
@@ -22,4 +34,20 @@ fun Modifier.ignoreVerticalParentPadding(vertical: Dp): Modifier {
|
|||||||
placeable.place(0, 0)
|
placeable.place(0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user