Реализован UnlockManager

This commit is contained in:
2025-01-18 21:24:41 +03:00
parent c5c0173391
commit b9e73cf197
25 changed files with 598 additions and 91 deletions

View File

@@ -3,7 +3,7 @@ package com.github.nullptroma.wallenc.app.di.modules.data
import android.content.Context
import com.github.nullptroma.wallenc.data.db.RoomFactory
import com.github.nullptroma.wallenc.data.db.app.IAppDb
import com.github.nullptroma.wallenc.data.db.app.dao.StorageKeyDao
import com.github.nullptroma.wallenc.data.db.app.dao.StorageKeyMapDao
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -21,8 +21,8 @@ class RoomModule {
@Provides
@Singleton
fun provideStorageKeyDao(database: IAppDb): StorageKeyDao {
return database.storageKeyDao
fun provideStorageKeyDao(database: IAppDb): StorageKeyMapDao {
return database.storageKeyMapDao
}
@Provides

View File

@@ -2,7 +2,8 @@ package com.github.nullptroma.wallenc.app.di.modules.data
import android.content.Context
import com.github.nullptroma.wallenc.app.di.modules.app.IoDispatcher
import com.github.nullptroma.wallenc.data.db.app.dao.StorageKeyDao
import com.github.nullptroma.wallenc.data.db.app.dao.StorageKeyMapDao
import com.github.nullptroma.wallenc.data.db.app.repository.StorageKeyMapRepository
import com.github.nullptroma.wallenc.data.vaults.UnlockManager
import com.github.nullptroma.wallenc.data.vaults.VaultsManager
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
@@ -25,13 +26,21 @@ class SingletonModule {
return VaultsManager(ioDispatcher, context)
}
@Provides
@Singleton
fun provideStorageKeyMapRepository(dao: StorageKeyMapDao): StorageKeyMapRepository {
return StorageKeyMapRepository(dao)
}
@Provides
@Singleton
fun provideUnlockManager(@IoDispatcher ioDispatcher: CoroutineDispatcher,
dao: StorageKeyDao): IUnlockManager {
repo: StorageKeyMapRepository,
vaultsManager: IVaultsManager): IUnlockManager {
return UnlockManager(
dao = dao,
ioDispatcher = ioDispatcher
repo = repo,
ioDispatcher = ioDispatcher,
vaultsManager = vaultsManager
)
}
}

View File

@@ -1,7 +1,9 @@
package com.github.nullptroma.wallenc.app.di.modules.domain
import com.github.nullptroma.wallenc.data.vaults.UnlockManager
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
import com.github.nullptroma.wallenc.domain.usecases.GetAllRawStoragesUseCase
import com.github.nullptroma.wallenc.domain.usecases.GetOpenedStoragesUseCase
import com.github.nullptroma.wallenc.domain.usecases.ManageLocalVaultUseCase
import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase
import dagger.Module
@@ -15,14 +17,14 @@ import javax.inject.Singleton
class UseCasesModule {
@Provides
@Singleton
fun provideGetAllRawStoragesUseCase(vaultsManager: IVaultsManager): GetAllRawStoragesUseCase {
return GetAllRawStoragesUseCase(vaultsManager)
fun provideGetOpenedStoragesUseCase(unlockManager: IUnlockManager): GetOpenedStoragesUseCase {
return GetOpenedStoragesUseCase(unlockManager)
}
@Provides
@Singleton
fun provideManageLocalVaultUseCase(vaultsManager: IVaultsManager): ManageLocalVaultUseCase {
return ManageLocalVaultUseCase(vaultsManager)
fun provideManageLocalVaultUseCase(vaultsManager: IVaultsManager, unlockManager: IUnlockManager): ManageLocalVaultUseCase {
return ManageLocalVaultUseCase(vaultsManager, unlockManager)
}
@Provides

View File

@@ -2,14 +2,14 @@ package com.github.nullptroma.wallenc.data.db.app
import androidx.room.Database
import androidx.room.RoomDatabase
import com.github.nullptroma.wallenc.data.db.app.dao.StorageKeyDao
import com.github.nullptroma.wallenc.data.db.app.model.DbStorageKey
import com.github.nullptroma.wallenc.data.db.app.dao.StorageKeyMapDao
import com.github.nullptroma.wallenc.data.db.app.model.DbStorageKeyMap
interface IAppDb {
val storageKeyDao: StorageKeyDao
val storageKeyMapDao: StorageKeyMapDao
}
@Database(entities = [DbStorageKey::class], version = 1, exportSchema = false)
@Database(entities = [DbStorageKeyMap::class], version = 2, exportSchema = false)
abstract class AppDb : IAppDb, RoomDatabase() {
abstract override val storageKeyDao: StorageKeyDao
abstract override val storageKeyMapDao: StorageKeyMapDao
}

View File

@@ -1,9 +0,0 @@
package com.github.nullptroma.wallenc.data.db.app.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@Dao
interface StorageKeyDao {
}

View File

@@ -0,0 +1,22 @@
package com.github.nullptroma.wallenc.data.db.app.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.github.nullptroma.wallenc.data.db.app.model.DbStorageKeyMap
import kotlinx.coroutines.flow.StateFlow
@Dao
interface StorageKeyMapDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun add(keymap: DbStorageKeyMap)
@Query("SELECT * FROM storage_key_maps")
fun getAll(): List<DbStorageKeyMap>
@Delete
fun delete(keymap: DbStorageKeyMap)
}

View File

@@ -1,11 +0,0 @@
package com.github.nullptroma.wallenc.data.db.app.model
import androidx.room.ColumnInfo
import androidx.room.Entity
@Entity(tableName = "storage_keys", primaryKeys = [ "source_uuid", "dest_uuid" ])
data class DbStorageKey(
@ColumnInfo(name = "source_uuid") val sourceUuid: String,
@ColumnInfo(name = "dest_uuid") val destUuid: String,
@ColumnInfo(name = "key") val key: String
)

View File

@@ -0,0 +1,53 @@
package com.github.nullptroma.wallenc.data.db.app.model
import androidx.room.ColumnInfo
import androidx.room.Entity
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 java.util.UUID
@Entity(tableName = "storage_key_maps", primaryKeys = [ "source_uuid", "dest_uuid" ])
data class DbStorageKeyMap(
@ColumnInfo(name = "source_uuid") val sourceUuid: String,
@ColumnInfo(name = "dest_uuid") val destUuid: String,
@ColumnInfo(name = "key") val key: ByteArray
) {
fun toModel(): StorageKeyMap {
return StorageKeyMap(
sourceUuid = UUID.fromString(sourceUuid),
destUuid = UUID.fromString(destUuid),
key = EncryptKey(key)
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as DbStorageKeyMap
if (sourceUuid != other.sourceUuid) return false
if (destUuid != other.destUuid) return false
if (!key.contentEquals(other.key)) return false
return true
}
override fun hashCode(): Int {
var result = sourceUuid.hashCode()
result = 31 * result + destUuid.hashCode()
result = 31 * result + key.contentHashCode()
return result
}
companion object {
fun fromModel(keymap: StorageKeyMap): DbStorageKeyMap {
return DbStorageKeyMap(
sourceUuid = keymap.sourceUuid.toString(),
destUuid = keymap.destUuid.toString(),
key = keymap.key.bytes
)
}
}
}

View File

@@ -0,0 +1,17 @@
package com.github.nullptroma.wallenc.data.db.app.repository
import com.github.nullptroma.wallenc.data.db.app.dao.StorageKeyMapDao
import com.github.nullptroma.wallenc.data.db.app.model.DbStorageKeyMap
import com.github.nullptroma.wallenc.data.model.StorageKeyMap
class StorageKeyMapRepository(private val dao: StorageKeyMapDao) {
fun getAll() = dao.getAll().map { it.toModel() }
fun add(keymap: StorageKeyMap) {
val dbModel = DbStorageKeyMap.fromModel(keymap)
dao.add(dbModel)
}
fun delete(keymap: StorageKeyMap) {
val dbModel = DbStorageKeyMap.fromModel(keymap)
dao.delete(dbModel)
}
}

View File

@@ -0,0 +1,11 @@
package com.github.nullptroma.wallenc.data.model
import androidx.room.ColumnInfo
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
import java.util.UUID
data class StorageKeyMap(
val sourceUuid: UUID,
val destUuid: UUID,
val key: EncryptKey
)

View File

@@ -1,34 +1,106 @@
package com.github.nullptroma.wallenc.data.vaults
import com.github.nullptroma.wallenc.data.db.app.dao.StorageKeyDao
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.domain.encrypt.EncryptedStorage
import com.github.nullptroma.wallenc.domain.encrypt.Encryptor
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.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext
import java.util.UUID
class UnlockManager(dao: StorageKeyDao, ioDispatcher: CoroutineDispatcher): IUnlockManager {
private val _openedStorages = MutableStateFlow<Map<UUID, EncryptedStorage>>(mapOf())
override val openedStorages: StateFlow<Map<UUID, IStorage>>
class UnlockManager(
private val repo: 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
val mutex = Mutex()
override fun open(
init {
CoroutineScope(ioDispatcher).launch {
vaultsManager.allStorages.collectLatest {
mutex.lock()
val allKeys = repo.getAll()
val allStorages = it.associateBy({ it.uuid }, { it })
val map = _openedStorages.value?.toMutableMap() ?: mutableMapOf()
for(keymap in allKeys) {
if(map.contains(keymap.sourceUuid))
continue
val storage = allStorages[keymap.sourceUuid] ?: continue
val encStorage = createEncryptedStorage(storage, keymap.key, keymap.destUuid)
map[storage.uuid] = encStorage
}
_openedStorages.value = map
mutex.unlock()
}
}
}
private fun createEncryptedStorage(storage: IStorage, key: EncryptKey, uuid: UUID): EncryptedStorage {
return EncryptedStorage(
source = storage,
key = key,
ioDispatcher = ioDispatcher,
uuid = uuid
)
}
override suspend fun open(
storage: IStorage,
key: EncryptKey
) {
TODO("Not yet implemented")
) = withContext(ioDispatcher) {
mutex.lock()
val encInfo = storage.encInfo.value ?: throw Exception("EncInfo is null") // TODO
if (!Encryptor.checkKey(key, encInfo))
throw Exception("Incorrect Key")
if (_openedStorages.value == null) {
val childScope = CoroutineScope(ioDispatcher)
}
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
repo.add(keymap)
mutex.unlock()
}
override fun close(uuid: UUID) {
val enc = _openedStorages.value[uuid]
if(enc == null)
return
_openedStorages.value = _openedStorages.value.toMutableMap().apply {
remove(uuid)
override suspend fun close(storage: IStorage) = withContext(ioDispatcher) {
mutex.lock()
val opened = _openedStorages.first { it != null }!!
val enc = opened[storage.uuid] ?: return@withContext
val model = StorageKeyMap(
sourceUuid = storage.uuid,
destUuid = enc.uuid,
key = EncryptKey("")
)
_openedStorages.value = opened.toMutableMap().apply {
remove(storage.uuid)
}
enc.dispose()
repo.delete(model)
mutex.unlock()
}
}

View File

@@ -2,6 +2,7 @@ package com.github.nullptroma.wallenc.data.vaults
import android.content.Context
import com.github.nullptroma.wallenc.data.vaults.local.LocalVault
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IVault
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
import kotlinx.coroutines.CoroutineDispatcher
@@ -12,6 +13,8 @@ class VaultsManager(ioDispatcher: CoroutineDispatcher, context: Context) : IVaul
override val remoteVaults: StateFlow<List<IVault>>
get() = TODO("Not yet implemented")
override val allStorages: StateFlow<List<IStorage>>
get() = localVault.storages
override fun addYandexVault(email: String, token: String) {
TODO("Not yet implemented")

View File

@@ -77,8 +77,9 @@ class LocalVault(private val ioDispatcher: CoroutineDispatcher, context: Context
override suspend fun createStorage(
enc: StorageEncryptionInfo
): LocalStorage = withContext(ioDispatcher) {
TODO("Not yet implemented")
val storage = createStorage()
storage.setEncInfo(enc)
return@withContext storage
}
override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) {

View File

@@ -3,13 +3,19 @@ package com.github.nullptroma.wallenc.domain.datatypes
import java.security.MessageDigest
import javax.crypto.spec.SecretKeySpec
class EncryptKey(val key: String) {
fun to32Bytes(): ByteArray {
class EncryptKey {
val bytes: ByteArray
constructor(password: String) {
val digest = MessageDigest.getInstance("SHA-256")
return digest.digest(key.toByteArray(Charsets.UTF_8))
bytes = digest.digest(password.toByteArray(Charsets.UTF_8))
}
constructor(key: ByteArray) {
this.bytes = key.clone()
}
fun toAesKey() : SecretKeySpec {
return SecretKeySpec(to32Bytes(), "AES")
return SecretKeySpec(bytes, "AES")
}
}

View File

@@ -6,29 +6,33 @@ import com.github.nullptroma.wallenc.domain.interfaces.ILogger
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
class EncryptedStorage(
source: IStorage,
private val source: IStorage,
key: EncryptKey,
logger: ILogger,
ioDispatcher: CoroutineDispatcher,
override val uuid: UUID = UUID.randomUUID()
) : IStorage, DisposableHandle {
override val size: StateFlow<Long?>
get() = TODO("Not yet implemented")
get() = source.size
override val numberOfFiles: StateFlow<Int?>
get() = TODO("Not yet implemented")
override val uuid: UUID
get() = TODO("Not yet implemented")
get() = source.numberOfFiles
override val name: StateFlow<String>
get() = TODO("Not yet implemented")
override val isAvailable: StateFlow<Boolean>
get() = TODO("Not yet implemented")
override val encInfo: StateFlow<StorageEncryptionInfo>
get() = TODO("Not yet implemented")
get() = source.isAvailable
override val encInfo: StateFlow<StorageEncryptionInfo?>
get() = MutableStateFlow(
StorageEncryptionInfo(
isEncrypted = false,
encryptedTestData = null
)
)
override val accessor: EncryptedStorageAccessor =
EncryptedStorageAccessor(source.accessor, key, logger, ioDispatcher)
EncryptedStorageAccessor(source.accessor, key, ioDispatcher)
override suspend fun rename(newName: String) {
TODO("Not yet implemented")

View File

@@ -28,7 +28,6 @@ import kotlin.io.path.pathString
class EncryptedStorageAccessor(
private val source: IStorageAccessor,
key: EncryptKey,
private val logger: ILogger,
ioDispatcher: CoroutineDispatcher
) : IStorageAccessor, DisposableHandle {
private val _job = Job()

View File

@@ -8,8 +8,8 @@ interface IUnlockManager {
/**
* Хранилища, для которых есть ключ шифрования
*/
val openedStorages: StateFlow<Map<UUID, IStorage>>
val openedStorages: StateFlow<Map<UUID, IStorage>?>
fun open(storage: IStorage, key: EncryptKey)
fun close(storage: UUID)
suspend fun open(storage: IStorage, key: EncryptKey)
suspend fun close(storage: IStorage)
}

View File

@@ -6,5 +6,6 @@ interface IVaultsManager {
val localVault: IVault
val remoteVaults: StateFlow<List<IVault>>
val allStorages: StateFlow<List<IStorage>>
fun addYandexVault(email: String, token: String)
}

View File

@@ -1,8 +0,0 @@
package com.github.nullptroma.wallenc.domain.usecases
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
class GetAllRawStoragesUseCase(private val manager: IVaultsManager) {
val localStorages
get() = manager.localVault.storages
}

View File

@@ -0,0 +1,12 @@
package com.github.nullptroma.wallenc.domain.usecases
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import java.util.UUID
class GetOpenedStoragesUseCase(private val unlockManager: IUnlockManager) {
val openedStorages: StateFlow<Map<UUID, IStorageInfo>?>
get() = unlockManager.openedStorages
}

View File

@@ -1,12 +1,24 @@
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.IStorageInfo
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
class ManageLocalVaultUseCase(private val manager: IVaultsManager) {
val localStorages
class ManageLocalVaultUseCase(private val manager: IVaultsManager, private val unlockManager: IUnlockManager) {
val localStorages: StateFlow<List<IStorageInfo>>
get() = manager.localVault.storages
suspend fun createStorage() {
manager.localVault.createStorage()
}
suspend fun createStorage(key: EncryptKey) {
val encInfo = Encryptor.generateEncryptionInfo(key)
val storage = manager.localVault.createStorage(encInfo)
unlockManager.open(storage, key)
}
}

View File

@@ -1,5 +1,5 @@
[versions]
agp = "8.7.1"
agp = "8.8.0"
jacksonModuleKotlin = "2.18.2"
kotlin = "2.0.10"
coreKtx = "1.15.0"
@@ -10,9 +10,9 @@ kotlinReflect = "2.0.21"
kotlinxCoroutinesCore = "1.9.0"
kotlinxSerializationJson = "1.7.3"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.9.3"
composeBom = "2024.10.01"
navigation = "2.8.3"
activityCompose = "1.10.0"
composeBom = "2025.01.00"
navigation = "2.8.5"
hiltNavigation = "1.2.0"
timber = "5.0.1"
yandexAuthSdk = "3.1.2"
@@ -68,7 +68,6 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" }
[plugins]

View File

@@ -1,6 +1,6 @@
#Sat Sep 07 01:04:14 MSK 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -1,15 +1,20 @@
package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault
import androidx.lifecycle.viewModelScope
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
import com.github.nullptroma.wallenc.domain.interfaces.IDirectory
import com.github.nullptroma.wallenc.domain.interfaces.IFile
import com.github.nullptroma.wallenc.domain.interfaces.ILogger
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
import com.github.nullptroma.wallenc.domain.usecases.GetOpenedStoragesUseCase
import com.github.nullptroma.wallenc.domain.usecases.ManageLocalVaultUseCase
import com.github.nullptroma.wallenc.domain.usecases.StorageFileManagementUseCase
import com.github.nullptroma.wallenc.presentation.extensions.toPrintable
import com.github.nullptroma.wallenc.presentation.viewmodel.ViewModelBase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.system.measureTimeMillis
@@ -17,13 +22,16 @@ import kotlin.system.measureTimeMillis
@HiltViewModel
class LocalVaultViewModel @Inject constructor(
private val _manageLocalVaultUseCase: ManageLocalVaultUseCase,
private val _getOpenedStoragesUseCase: GetOpenedStoragesUseCase,
private val _storageFileManagementUseCase: StorageFileManagementUseCase,
private val logger: ILogger
) :
ViewModelBase<LocalVaultScreenState>(LocalVaultScreenState(listOf())) {
init {
viewModelScope.launch {
_manageLocalVaultUseCase.localStorages.collect {
_manageLocalVaultUseCase.localStorages.combine(_getOpenedStoragesUseCase.openedStorages) { local, opened ->
local + (opened?.map { it.value } ?: listOf())
}.collectLatest {
val newState = state.value.copy(
storagesList = it
)
@@ -54,7 +62,7 @@ class LocalVaultViewModel @Inject constructor(
fun createStorage() {
viewModelScope.launch {
_manageLocalVaultUseCase.createStorage()
_manageLocalVaultUseCase.createStorage(EncryptKey("hello"))
}
}
}

View File

@@ -82,6 +82,8 @@ controlflow {
<reflist>
<ref refid="4652e56b-9248-11ef-8256-d5c6949dbfe2"/>
<ref refid="4dc1ae5b-9249-11ef-80b9-d5c6949dbfe2"/>
<ref refid="c7dfafe9-cf20-11ef-ac08-bf6aa1e99673"/>
<ref refid="cbe7db79-cf20-11ef-9e8c-bf6aa1e99673"/>
</reflist>
</nestedPackage>
<ownedDiagram>
@@ -341,6 +343,13 @@ existing classes or even new classes with specific responsibilities.</val>
<reflist>
<ref refid="98c57477-9249-11ef-91a4-d5c6949dbfe2"/>
<ref refid="9aa5be3e-9249-11ef-80b4-d5c6949dbfe2"/>
<ref refid="c7e02426-cf20-11ef-946a-bf6aa1e99673"/>
<ref refid="cbe85988-cf20-11ef-88a2-bf6aa1e99673"/>
<ref refid="d9feb676-cf20-11ef-9ba9-bf6aa1e99673"/>
<ref refid="dbd3c61b-cf20-11ef-8f96-bf6aa1e99673"/>
<ref refid="dd4e81d8-cf20-11ef-920e-bf6aa1e99673"/>
<ref refid="e1401072-cf20-11ef-b6f2-bf6aa1e99673"/>
<ref refid="e329e455-cf20-11ef-aab2-bf6aa1e99673"/>
</reflist>
</ownedPresentation>
</Diagram>
@@ -410,6 +419,13 @@ existing classes or even new classes with specific responsibilities.</val>
<ref refid="9aa5be3e-9249-11ef-80b4-d5c6949dbfe2"/>
</reflist>
</presentation>
<supplierDependency>
<reflist>
<ref refid="da66c106-cf20-11ef-b314-bf6aa1e99673"/>
<ref refid="dc38c0ec-cf20-11ef-ae61-bf6aa1e99673"/>
<ref refid="ddb583fd-cf20-11ef-9111-bf6aa1e99673"/>
</reflist>
</supplierDependency>
</Package>
<Diagram id="7a8a9c2d-9248-11ef-bfb6-d5c6949dbfe2">
<diagramType>
@@ -461,6 +477,11 @@ existing classes or even new classes with specific responsibilities.</val>
</ownedPresentation>
</Diagram>
<Package id="4dc1ae5b-9249-11ef-80b9-d5c6949dbfe2">
<clientDependency>
<reflist>
<ref refid="ddb583fd-cf20-11ef-9111-bf6aa1e99673"/>
</reflist>
</clientDependency>
<name>
<val>Data</val>
</name>
@@ -477,6 +498,11 @@ existing classes or even new classes with specific responsibilities.</val>
<ref refid="98c57477-9249-11ef-91a4-d5c6949dbfe2"/>
</reflist>
</presentation>
<supplierDependency>
<reflist>
<ref refid="e37cfbf9-cf20-11ef-bb0a-bf6aa1e99673"/>
</reflist>
</supplierDependency>
</Package>
<Diagram id="647635fd-9249-11ef-9b9a-d5c6949dbfe2">
<diagramType>
@@ -491,7 +517,7 @@ existing classes or even new classes with specific responsibilities.</val>
</Diagram>
<PackageItem id="98c57477-9249-11ef-91a4-d5c6949dbfe2">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, -166.66017150878906, 7.74609375)</val>
<val>(1.0, 0.0, 0.0, 1.0, -169.66725884984865, 289.74999999999994)</val>
</matrix>
<top-left>
<val>(0.0, 0.0)</val>
@@ -511,10 +537,10 @@ existing classes or even new classes with specific responsibilities.</val>
</PackageItem>
<PackageItem id="9aa5be3e-9249-11ef-80b4-d5c6949dbfe2">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 171.25390625, 4.573046875000017)</val>
<val>(1.0, 0.0, 0.0, 1.0, 168.02553396117582, 0.006953125000009663)</val>
</matrix>
<top-left>
<val>(-9.948932077648351, 0.0)</val>
<val>(0.0, 0.0)</val>
</top-left>
<width>
<val>171.94893207764835</val>
@@ -3681,4 +3707,282 @@ existing classes or even new classes with specific responsibilities.</val>
<ref refid="ed56560a-ca21-11ef-a923-d31b240a181b"/>
</supplier>
</Usage>
<Package id="c7dfafe9-cf20-11ef-ac08-bf6aa1e99673">
<clientDependency>
<reflist>
<ref refid="dc38c0ec-cf20-11ef-ae61-bf6aa1e99673"/>
<ref refid="e1b05c21-cf20-11ef-a005-bf6aa1e99673"/>
<ref refid="e37cfbf9-cf20-11ef-bb0a-bf6aa1e99673"/>
</reflist>
</clientDependency>
<name>
<val>App</val>
</name>
<package>
<ref refid="f3a82730-71b1-11ec-a409-f47b099bf663"/>
</package>
<presentation>
<reflist>
<ref refid="c7e02426-cf20-11ef-946a-bf6aa1e99673"/>
</reflist>
</presentation>
</Package>
<PackageItem id="c7e02426-cf20-11ef-946a-bf6aa1e99673">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 188.5, 540.3484910295214)</val>
</matrix>
<top-left>
<val>(0.0, 0.0)</val>
</top-left>
<width>
<val>131.0</val>
</width>
<height>
<val>70.0</val>
</height>
<diagram>
<ref refid="585f224c-71b6-11ec-a409-f47b099bf663"/>
</diagram>
<subject>
<ref refid="c7dfafe9-cf20-11ef-ac08-bf6aa1e99673"/>
</subject>
</PackageItem>
<Package id="cbe7db79-cf20-11ef-9e8c-bf6aa1e99673">
<clientDependency>
<reflist>
<ref refid="da66c106-cf20-11ef-b314-bf6aa1e99673"/>
</reflist>
</clientDependency>
<name>
<val>Presentation</val>
</name>
<package>
<ref refid="f3a82730-71b1-11ec-a409-f47b099bf663"/>
</package>
<presentation>
<reflist>
<ref refid="cbe85988-cf20-11ef-88a2-bf6aa1e99673"/>
</reflist>
</presentation>
<supplierDependency>
<reflist>
<ref refid="e1b05c21-cf20-11ef-a005-bf6aa1e99673"/>
</reflist>
</supplierDependency>
</Package>
<PackageItem id="cbe85988-cf20-11ef-88a2-bf6aa1e99673">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 448.06283721542303, 334.1732530596131)</val>
</matrix>
<top-left>
<val>(0.0, 0.0)</val>
</top-left>
<width>
<val>125.0</val>
</width>
<height>
<val>70.0</val>
</height>
<diagram>
<ref refid="585f224c-71b6-11ec-a409-f47b099bf663"/>
</diagram>
<subject>
<ref refid="cbe7db79-cf20-11ef-9e8c-bf6aa1e99673"/>
</subject>
</PackageItem>
<DependencyItem id="d9feb676-cf20-11ef-9ba9-bf6aa1e99673">
<diagram>
<ref refid="585f224c-71b6-11ec-a409-f47b099bf663"/>
</diagram>
<horizontal>
<val>0</val>
</horizontal>
<orthogonal>
<val>0</val>
</orthogonal>
<subject>
<ref refid="da66c106-cf20-11ef-b314-bf6aa1e99673"/>
</subject>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, -60.19342961958796, 358.1635683335607)</val>
</matrix>
<points>
<val>[(345.8748167061808, -178.41052145856074), (559.786266835011, -23.9903152739476)]</val>
</points>
<head-connection>
<ref refid="9aa5be3e-9249-11ef-80b4-d5c6949dbfe2"/>
</head-connection>
<tail-connection>
<ref refid="cbe85988-cf20-11ef-88a2-bf6aa1e99673"/>
</tail-connection>
</DependencyItem>
<Dependency id="da66c106-cf20-11ef-b314-bf6aa1e99673">
<client>
<ref refid="cbe7db79-cf20-11ef-9e8c-bf6aa1e99673"/>
</client>
<presentation>
<reflist>
<ref refid="d9feb676-cf20-11ef-9ba9-bf6aa1e99673"/>
</reflist>
</presentation>
<supplier>
<ref refid="4652e56b-9248-11ef-8256-d5c6949dbfe2"/>
</supplier>
</Dependency>
<DependencyItem id="dbd3c61b-cf20-11ef-8f96-bf6aa1e99673">
<diagram>
<ref refid="585f224c-71b6-11ec-a409-f47b099bf663"/>
</diagram>
<horizontal>
<val>0</val>
</horizontal>
<orthogonal>
<val>0</val>
</orthogonal>
<subject>
<ref refid="dc38c0ec-cf20-11ef-ae61-bf6aa1e99673"/>
</subject>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 220.8934129474165, 355.288804110198)</val>
</matrix>
<points>
<val>[(37.80897389480657, -175.53575723519802), (33.10658705258351, -102.53880411019799), (33.10658705258351, 144.861195889802), (33.10658705258351, 185.05968691932338)]</val>
</points>
<head-connection>
<ref refid="9aa5be3e-9249-11ef-80b4-d5c6949dbfe2"/>
</head-connection>
<tail-connection>
<ref refid="c7e02426-cf20-11ef-946a-bf6aa1e99673"/>
</tail-connection>
</DependencyItem>
<Dependency id="dc38c0ec-cf20-11ef-ae61-bf6aa1e99673">
<client>
<ref refid="c7dfafe9-cf20-11ef-ac08-bf6aa1e99673"/>
</client>
<presentation>
<reflist>
<ref refid="dbd3c61b-cf20-11ef-8f96-bf6aa1e99673"/>
</reflist>
</presentation>
<supplier>
<ref refid="4652e56b-9248-11ef-8256-d5c6949dbfe2"/>
</supplier>
</Dependency>
<DependencyItem id="dd4e81d8-cf20-11ef-920e-bf6aa1e99673">
<diagram>
<ref refid="585f224c-71b6-11ec-a409-f47b099bf663"/>
</diagram>
<horizontal>
<val>0</val>
</horizontal>
<orthogonal>
<val>0</val>
</orthogonal>
<subject>
<ref refid="ddb583fd-cf20-11ef-9111-bf6aa1e99673"/>
</subject>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 470.30166841962773, 308.92757236917936)</val>
</matrix>
<points>
<val>[(-253.4101148222706, -129.1745254941794), (-484.34892726947635, -19.17757236917936)]</val>
</points>
<head-connection>
<ref refid="9aa5be3e-9249-11ef-80b4-d5c6949dbfe2"/>
</head-connection>
<tail-connection>
<ref refid="98c57477-9249-11ef-91a4-d5c6949dbfe2"/>
</tail-connection>
</DependencyItem>
<Dependency id="ddb583fd-cf20-11ef-9111-bf6aa1e99673">
<client>
<ref refid="4dc1ae5b-9249-11ef-80b9-d5c6949dbfe2"/>
</client>
<presentation>
<reflist>
<ref refid="dd4e81d8-cf20-11ef-920e-bf6aa1e99673"/>
</reflist>
</presentation>
<supplier>
<ref refid="4652e56b-9248-11ef-8256-d5c6949dbfe2"/>
</supplier>
</Dependency>
<DependencyItem id="e1401072-cf20-11ef-b6f2-bf6aa1e99673">
<diagram>
<ref refid="585f224c-71b6-11ec-a409-f47b099bf663"/>
</diagram>
<horizontal>
<val>0</val>
</horizontal>
<orthogonal>
<val>0</val>
</orthogonal>
<subject>
<ref refid="e1b05c21-cf20-11ef-a005-bf6aa1e99673"/>
</subject>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 171.43286989350588, 363.845719493801)</val>
</matrix>
<points>
<val>[(346.31019988005676, 40.327533565812075), (96.13713010649411, 176.50277153572034)]</val>
</points>
<head-connection>
<ref refid="cbe85988-cf20-11ef-88a2-bf6aa1e99673"/>
</head-connection>
<tail-connection>
<ref refid="c7e02426-cf20-11ef-946a-bf6aa1e99673"/>
</tail-connection>
</DependencyItem>
<Dependency id="e1b05c21-cf20-11ef-a005-bf6aa1e99673">
<client>
<ref refid="c7dfafe9-cf20-11ef-ac08-bf6aa1e99673"/>
</client>
<presentation>
<reflist>
<ref refid="e1401072-cf20-11ef-b6f2-bf6aa1e99673"/>
</reflist>
</presentation>
<supplier>
<ref refid="cbe7db79-cf20-11ef-9e8c-bf6aa1e99673"/>
</supplier>
</Dependency>
<DependencyItem id="e329e455-cf20-11ef-aab2-bf6aa1e99673">
<diagram>
<ref refid="585f224c-71b6-11ec-a409-f47b099bf663"/>
</diagram>
<horizontal>
<val>0</val>
</horizontal>
<orthogonal>
<val>0</val>
</orthogonal>
<subject>
<ref refid="e37cfbf9-cf20-11ef-bb0a-bf6aa1e99673"/>
</subject>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 243.35250844243774, 348.8318141553794)</val>
</matrix>
<points>
<val>[(-247.11976729228638, 114.31818584462059), (-10.452508442437733, 191.51667687414198)]</val>
</points>
<head-connection>
<ref refid="98c57477-9249-11ef-91a4-d5c6949dbfe2"/>
</head-connection>
<tail-connection>
<ref refid="c7e02426-cf20-11ef-946a-bf6aa1e99673"/>
</tail-connection>
</DependencyItem>
<Dependency id="e37cfbf9-cf20-11ef-bb0a-bf6aa1e99673">
<client>
<ref refid="c7dfafe9-cf20-11ef-ac08-bf6aa1e99673"/>
</client>
<presentation>
<reflist>
<ref refid="e329e455-cf20-11ef-aab2-bf6aa1e99673"/>
</reflist>
</presentation>
<supplier>
<ref refid="4dc1ae5b-9249-11ef-80b9-d5c6949dbfe2"/>
</supplier>
</Dependency>
</gaphor>