Большой рефакторинг
Из domain выкинуты типы vault, теперь он ничего не знает о Yandex. Объявления провайдеров вынесены в vault-api, а реализации в data
This commit is contained in:
@@ -54,4 +54,5 @@ dependencies {
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
implementation(project(":domain"))
|
||||
implementation(project(":vault-api"))
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.github.nullptroma.wallenc.data.auth
|
||||
|
||||
/**
|
||||
* Scope-ы Яндекс.OAuth, которые нам нужны: только app_folder + disk.info.
|
||||
*
|
||||
* Используется как ссылка для синхронизации с консолью Yandex OAuth.
|
||||
* Сам Yandex Auth SDK для WEBVIEW-логина запрашивает scope-ы, выставленные
|
||||
* у приложения в OAuth-консоли; мы держим список здесь, чтобы было одно место правды.
|
||||
*/
|
||||
object YandexOAuthScopes {
|
||||
const val DISK_APP_FOLDER = "cloud_api:disk.app_folder"
|
||||
const val DISK_INFO = "cloud_api:disk.info"
|
||||
|
||||
val ALL: Set<String> = setOf(DISK_APP_FOLDER, DISK_INFO)
|
||||
}
|
||||
@@ -6,10 +6,16 @@ import com.github.nullptroma.wallenc.data.db.app.repository.StorageKeyMapReposit
|
||||
import com.github.nullptroma.wallenc.data.db.app.repository.YandexAccountRepository
|
||||
import com.github.nullptroma.wallenc.data.network.yandexuserinfo.repository.YandexUserInfoRepository
|
||||
import com.github.nullptroma.wallenc.data.storages.UnlockManager
|
||||
import com.github.nullptroma.wallenc.data.vaults.local.LocalVault
|
||||
import com.github.nullptroma.wallenc.data.vaults.local.LocalVaultIdStore
|
||||
import com.github.nullptroma.wallenc.data.vaults.yandex.YandexRegistration
|
||||
import com.github.nullptroma.wallenc.data.vaults.yandex.YandexVault
|
||||
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.IVaultsManager
|
||||
import com.github.nullptroma.wallenc.vaultapi.VaultRegistrar
|
||||
import com.github.nullptroma.wallenc.vaultapi.VaultRegistration
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -26,21 +32,17 @@ class VaultsManager(
|
||||
keyRepo: StorageKeyMapRepository,
|
||||
private val yandexAccountRepository: YandexAccountRepository,
|
||||
private val yandexUserInfoRepository: YandexUserInfoRepository,
|
||||
) : IVaultsManager {
|
||||
) : IVaultsManager, VaultRegistrar {
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + ioDispatcher)
|
||||
|
||||
override val localVault = LocalVault(ioDispatcher, context)
|
||||
|
||||
// До unlockManager: UnlockManager в init подписывается на allStorages.
|
||||
override val allStorages: StateFlow<List<IStorage>> = localVault.storages
|
||||
|
||||
override val unlockManager: IUnlockManager = UnlockManager(
|
||||
keymapRepository = keyRepo,
|
||||
private val localVault: IVault = LocalVault(
|
||||
ioDispatcher = ioDispatcher,
|
||||
vaultsManager = this
|
||||
context = context,
|
||||
idStore = LocalVaultIdStore(context),
|
||||
)
|
||||
|
||||
private val _remoteVaults = yandexAccountRepository.observeAll()
|
||||
private val yandexVaults: StateFlow<List<IVault>> = yandexAccountRepository.observeAll()
|
||||
.map { rows ->
|
||||
rows.map { row ->
|
||||
YandexVault(
|
||||
@@ -52,33 +54,51 @@ class VaultsManager(
|
||||
}
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
override val remoteVaults: StateFlow<List<IVault>> = _remoteVaults
|
||||
|
||||
override val allVaults: StateFlow<List<IVault>> = _remoteVaults
|
||||
.map { listOf(localVault) + it }
|
||||
override val vaults: StateFlow<List<IVault>> = yandexVaults
|
||||
.map { remote -> listOf(localVault) + remote }
|
||||
.stateIn(scope, SharingStarted.Eagerly, listOf(localVault))
|
||||
|
||||
override suspend fun addYandexVault(accessToken: String) = withContext(ioDispatcher) {
|
||||
val info = yandexUserInfoRepository.userInfo(accessToken)
|
||||
// Поведение Phase 1 — UnlockManager работает только с локальными storages.
|
||||
// Расширение до combine(local + remote) пойдёт во Phase 2.
|
||||
override val allStorages: StateFlow<List<IStorage>> = localVault.storages
|
||||
|
||||
override val unlockManager: IUnlockManager = UnlockManager(
|
||||
keymapRepository = keyRepo,
|
||||
ioDispatcher = ioDispatcher,
|
||||
vaultsManager = this,
|
||||
)
|
||||
|
||||
override suspend fun register(registration: VaultRegistration) = withContext(ioDispatcher) {
|
||||
when (registration) {
|
||||
is YandexRegistration -> registerYandex(registration)
|
||||
else -> throw IllegalArgumentException(
|
||||
"Unknown VaultRegistration type: ${registration::class.qualifiedName}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun unregister(vaultUuid: UUID): Unit = withContext(ioDispatcher) {
|
||||
yandexAccountRepository.deleteByVaultUuid(vaultUuid.toString())
|
||||
}
|
||||
|
||||
private suspend fun registerYandex(registration: YandexRegistration) {
|
||||
val token = registration.oauthToken
|
||||
val info = yandexUserInfoRepository.userInfo(token)
|
||||
val email = info.defaultEmail?.takeIf { it.isNotBlank() }
|
||||
?: "${info.login}@yandex.ru"
|
||||
val existing = yandexAccountRepository.getByYandexUserId(info.id)
|
||||
val vaultUuid = existing?.vaultUuid ?: UUID.randomUUID().toString()
|
||||
if (existing != null) {
|
||||
yandexAccountRepository.updateCredentials(vaultUuid, email, accessToken)
|
||||
yandexAccountRepository.updateCredentials(vaultUuid, email, token)
|
||||
} else {
|
||||
yandexAccountRepository.insert(
|
||||
DbYandexAccount(
|
||||
vaultUuid = vaultUuid,
|
||||
yandexUserId = info.id,
|
||||
email = email,
|
||||
oauthToken = accessToken,
|
||||
)
|
||||
oauthToken = token,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeRemoteVault(vaultUuid: UUID) = withContext(ioDispatcher) {
|
||||
yandexAccountRepository.deleteByVaultUuid(vaultUuid.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.github.nullptroma.wallenc.data.vaults
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
||||
import com.github.nullptroma.wallenc.domain.enums.VaultType
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IYandexVault
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Удалённый vault Яндекс: OAuth и профиль есть; Yandex.Disk и хранилища — заглушки.
|
||||
*/
|
||||
class YandexVault(
|
||||
override val uuid: UUID,
|
||||
override val accountEmail: String,
|
||||
val oauthToken: String,
|
||||
) : IYandexVault {
|
||||
override val type: VaultType = VaultType.YANDEX
|
||||
|
||||
private val _storages = MutableStateFlow<List<IStorage>>(emptyList())
|
||||
override val storages: StateFlow<List<IStorage>?> = _storages
|
||||
|
||||
private val _isAvailable = MutableStateFlow(true)
|
||||
override val isAvailable: StateFlow<Boolean> = _isAvailable
|
||||
|
||||
private val _totalSpace = MutableStateFlow<Int?>(null)
|
||||
override val totalSpace: StateFlow<Int?> = _totalSpace
|
||||
|
||||
private val _availableSpace = MutableStateFlow<Int?>(null)
|
||||
override val availableSpace: StateFlow<Int?> = _availableSpace
|
||||
|
||||
override suspend fun createStorage(): IStorage {
|
||||
throw UnsupportedOperationException("Yandex.Disk ещё не подключён")
|
||||
}
|
||||
|
||||
override suspend fun createStorage(enc: StorageEncryptionInfo): IStorage {
|
||||
throw UnsupportedOperationException("Yandex.Disk ещё не подключён")
|
||||
}
|
||||
|
||||
override suspend fun remove(storage: IStorage) {
|
||||
// заглушка до интеграции API Диска
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.github.nullptroma.wallenc.data.vaults
|
||||
package com.github.nullptroma.wallenc.data.vaults.local
|
||||
|
||||
import android.content.Context
|
||||
import com.github.nullptroma.wallenc.data.storages.local.LocalStorage
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
||||
import com.github.nullptroma.wallenc.domain.enums.VaultType
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IVault
|
||||
import com.github.nullptroma.wallenc.vaultapi.DescribedVault
|
||||
import com.github.nullptroma.wallenc.vaultapi.VaultDescriptor
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -18,21 +18,26 @@ import kotlin.io.path.Path
|
||||
import kotlin.io.path.createDirectory
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
class LocalVault(private val ioDispatcher: CoroutineDispatcher, context: Context) : IVault {
|
||||
override val type: VaultType = VaultType.LOCAL
|
||||
override val uuid: UUID
|
||||
get() = TODO("Not yet implemented")
|
||||
class LocalVault(
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
context: Context,
|
||||
idStore: LocalVaultIdStore,
|
||||
) : DescribedVault {
|
||||
|
||||
private val _storages = MutableStateFlow(listOf<LocalStorage>())
|
||||
override val uuid: UUID = idStore.getOrCreate()
|
||||
|
||||
override val descriptor: VaultDescriptor = VaultDescriptor.LocalDevice(uuid)
|
||||
|
||||
private val _storages = MutableStateFlow<List<IStorage>>(emptyList())
|
||||
override val storages: StateFlow<List<IStorage>> = _storages
|
||||
|
||||
private val _isAvailable = MutableStateFlow(false)
|
||||
override val isAvailable: StateFlow<Boolean> = _isAvailable
|
||||
|
||||
private val _totalSpace = MutableStateFlow(null)
|
||||
private val _totalSpace = MutableStateFlow<Int?>(null)
|
||||
override val totalSpace: StateFlow<Int?> = _totalSpace
|
||||
|
||||
private val _availableSpace = MutableStateFlow(null)
|
||||
private val _availableSpace = MutableStateFlow<Int?>(null)
|
||||
override val availableSpace: StateFlow<Int?> = _availableSpace
|
||||
|
||||
private val path = MutableStateFlow<File?>(null)
|
||||
@@ -54,7 +59,7 @@ class LocalVault(private val ioDispatcher: CoroutineDispatcher, context: Context
|
||||
if (dirs != null) {
|
||||
_storages.value = dirs.map {
|
||||
val uuid = UUID.fromString(it.name)
|
||||
return@map LocalStorage(uuid, it.path, ioDispatcher).apply { init() }
|
||||
LocalStorage(uuid, it.path, ioDispatcher).apply { init() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +81,7 @@ class LocalVault(private val ioDispatcher: CoroutineDispatcher, context: Context
|
||||
}
|
||||
|
||||
override suspend fun createStorage(
|
||||
enc: StorageEncryptionInfo
|
||||
enc: StorageEncryptionInfo,
|
||||
): LocalStorage = withContext(ioDispatcher) {
|
||||
val storage = createStorage()
|
||||
storage.setEncInfo(enc)
|
||||
@@ -90,11 +95,11 @@ class LocalVault(private val ioDispatcher: CoroutineDispatcher, context: Context
|
||||
|
||||
val curStorages = _storages.value.toMutableList()
|
||||
val index = curStorages.indexOfFirst { it.uuid == storage.uuid }
|
||||
if(index != -1) {
|
||||
val localStorage = curStorages[index]
|
||||
if (index != -1) {
|
||||
val localStorage = curStorages[index] as LocalStorage
|
||||
curStorages.removeAt(index)
|
||||
_storages.value = curStorages
|
||||
File(localStorage.absolutePath).deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.github.nullptroma.wallenc.data.vaults.local
|
||||
|
||||
import android.content.Context
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Хранит/восстанавливает идентификатор единственного локального vault'а
|
||||
* в [android.content.SharedPreferences]. При первом обращении генерирует новый UUID
|
||||
* и записывает его синхронно (`commit`), чтобы к моменту, когда другие подсистемы
|
||||
* (DB, шифр-ключи) начнут связывать с ним записи, значение уже было персистентно.
|
||||
*/
|
||||
class LocalVaultIdStore(context: Context) {
|
||||
|
||||
private val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
/** Возвращает существующий идентификатор или, если его нет, генерирует и сохраняет новый. */
|
||||
fun getOrCreate(): UUID {
|
||||
prefs.getString(KEY_LOCAL_VAULT_UUID, null)?.let { stored ->
|
||||
runCatching { UUID.fromString(stored) }.getOrNull()?.let { return it }
|
||||
}
|
||||
val generated = UUID.randomUUID()
|
||||
prefs.edit().putString(KEY_LOCAL_VAULT_UUID, generated.toString()).commit()
|
||||
return generated
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PREFS_NAME = "wallenc.vaults"
|
||||
const val KEY_LOCAL_VAULT_UUID = "local_vault_uuid"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.github.nullptroma.wallenc.data.vaults.yandex
|
||||
|
||||
import com.github.nullptroma.wallenc.vaultapi.CloudBrand
|
||||
import com.github.nullptroma.wallenc.vaultapi.VaultRegistration
|
||||
|
||||
/**
|
||||
* Регистрация удалённого vault'а Яндекс.Диска по результату OAuth.
|
||||
*
|
||||
* Живёт в `:data` (а не в `:vault-api`), потому что [VaultRegistration]
|
||||
* намеренно не sealed — конкретные реализации лежат рядом со своим поставщиком.
|
||||
* presentation никогда не открывает этот тип, только перепасовывает обратно
|
||||
* в `VaultRegistrar.register(...)`.
|
||||
*/
|
||||
data class YandexRegistration(
|
||||
val oauthToken: String,
|
||||
) : VaultRegistration {
|
||||
init {
|
||||
require(oauthToken.isNotBlank()) { "oauthToken must not be blank" }
|
||||
}
|
||||
|
||||
val brand: CloudBrand get() = CloudBrand.YANDEX
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.github.nullptroma.wallenc.data.vaults.yandex
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||
import com.github.nullptroma.wallenc.vaultapi.CloudBrand
|
||||
import com.github.nullptroma.wallenc.vaultapi.DescribedVault
|
||||
import com.github.nullptroma.wallenc.vaultapi.VaultDescriptor
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Удалённый vault Яндекс.Диска. Сейчас — заглушка для уровня storages
|
||||
* (Phase 1 — только OAuth + перечисление аккаунтов).
|
||||
*/
|
||||
class YandexVault(
|
||||
override val uuid: UUID,
|
||||
accountEmail: String,
|
||||
val oauthToken: String,
|
||||
) : DescribedVault {
|
||||
|
||||
override val descriptor: VaultDescriptor = VaultDescriptor.LinkedRemote(
|
||||
uuid = uuid,
|
||||
brand = CloudBrand.YANDEX,
|
||||
accountDisplayName = accountEmail,
|
||||
)
|
||||
|
||||
private val _storages = MutableStateFlow<List<IStorage>>(emptyList())
|
||||
override val storages: StateFlow<List<IStorage>> = _storages
|
||||
|
||||
private val _isAvailable = MutableStateFlow(true)
|
||||
override val isAvailable: StateFlow<Boolean> = _isAvailable
|
||||
|
||||
private val _totalSpace = MutableStateFlow<Int?>(null)
|
||||
override val totalSpace: StateFlow<Int?> = _totalSpace
|
||||
|
||||
private val _availableSpace = MutableStateFlow<Int?>(null)
|
||||
override val availableSpace: StateFlow<Int?> = _availableSpace
|
||||
|
||||
override suspend fun createStorage(): IStorage =
|
||||
throw UnsupportedOperationException("Yandex.Disk REST integration is not connected yet")
|
||||
|
||||
override suspend fun createStorage(enc: StorageEncryptionInfo): IStorage =
|
||||
throw UnsupportedOperationException("Yandex.Disk REST integration is not connected yet")
|
||||
|
||||
override suspend fun remove(storage: IStorage) {
|
||||
// No-op до интеграции API Диска.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user