Большой рефакторинг
Из domain выкинуты типы vault, теперь он ничего не знает о Yandex. Объявления провайдеров вынесены в vault-api, а реализации в data
This commit is contained in:
@@ -77,4 +77,5 @@ dependencies {
|
|||||||
implementation(project(":domain"))
|
implementation(project(":domain"))
|
||||||
implementation(project(":data"))
|
implementation(project(":data"))
|
||||||
implementation(project(":presentation"))
|
implementation(project(":presentation"))
|
||||||
|
implementation(project(":vault-api"))
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,10 @@ package com.github.nullptroma.wallenc.app.auth
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import com.github.nullptroma.wallenc.domain.auth.RemoteYandexAuthResult
|
import com.github.nullptroma.wallenc.data.vaults.yandex.YandexRegistration
|
||||||
import com.github.nullptroma.wallenc.domain.auth.RemoteYandexSignInLauncher
|
import com.github.nullptroma.wallenc.vaultapi.CloudBrand
|
||||||
|
import com.github.nullptroma.wallenc.vaultapi.RemoteVaultAuthenticator
|
||||||
|
import com.github.nullptroma.wallenc.vaultapi.VaultLinkOutcome
|
||||||
import com.yandex.authsdk.YandexAuthLoginOptions
|
import com.yandex.authsdk.YandexAuthLoginOptions
|
||||||
import com.yandex.authsdk.YandexAuthOptions
|
import com.yandex.authsdk.YandexAuthOptions
|
||||||
import com.yandex.authsdk.YandexAuthResult
|
import com.yandex.authsdk.YandexAuthResult
|
||||||
@@ -21,11 +23,15 @@ import javax.inject.Singleton
|
|||||||
*
|
*
|
||||||
* При смене конфигурации новая Activity может вызвать [registerWith] до [unregister] старой —
|
* При смене конфигурации новая Activity может вызвать [registerWith] до [unregister] старой —
|
||||||
* тогда владелец и launcher обновляются; [unregister] для уже неактуального экземпляра — no-op.
|
* тогда владелец и launcher обновляются; [unregister] для уже неактуального экземпляра — no-op.
|
||||||
|
*
|
||||||
|
* Реализует [RemoteVaultAuthenticator] из `:vault-api`: presentation про Yandex SDK
|
||||||
|
* ничего не знает, а получает обобщённый [VaultLinkOutcome] с готовой
|
||||||
|
* `VaultRegistration` внутри.
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class YandexSignInService @Inject constructor(
|
class YandexSignInService @Inject constructor(
|
||||||
@ApplicationContext appContext: Context,
|
@ApplicationContext appContext: Context,
|
||||||
) : RemoteYandexSignInLauncher {
|
) : RemoteVaultAuthenticator {
|
||||||
|
|
||||||
private val sdk = YandexAuthSdk.create(YandexAuthOptions(appContext, true))
|
private val sdk = YandexAuthSdk.create(YandexAuthOptions(appContext, true))
|
||||||
|
|
||||||
@@ -34,7 +40,7 @@ class YandexSignInService @Inject constructor(
|
|||||||
private var registrationOwner: ComponentActivity? = null
|
private var registrationOwner: ComponentActivity? = null
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var pending: ((RemoteYandexAuthResult) -> Unit)? = null
|
private var pending: ((VaultLinkOutcome) -> Unit)? = null
|
||||||
|
|
||||||
fun registerWith(activity: ComponentActivity) {
|
fun registerWith(activity: ComponentActivity) {
|
||||||
if (registrationOwner === activity && launcher != null) return
|
if (registrationOwner === activity && launcher != null) return
|
||||||
@@ -59,25 +65,34 @@ class YandexSignInService @Inject constructor(
|
|||||||
pending = null
|
pending = null
|
||||||
p
|
p
|
||||||
}
|
}
|
||||||
cb?.invoke(RemoteYandexAuthResult.Cancelled)
|
cb?.invoke(VaultLinkOutcome.Cancelled)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun launch(onResult: (RemoteYandexAuthResult) -> Unit) {
|
override fun beginLink(brand: CloudBrand, onResult: (VaultLinkOutcome) -> Unit) {
|
||||||
val l = launcher
|
if (brand != CloudBrand.YANDEX) {
|
||||||
?: error("YandexSignInService: call registerWith(activity) from MainActivity.onCreate first")
|
onResult(VaultLinkOutcome.Failed("Brand $brand is not supported by YandexSignInService"))
|
||||||
synchronized(this) {
|
return
|
||||||
pending = onResult
|
|
||||||
}
|
}
|
||||||
|
val l = launcher
|
||||||
|
if (l == null) {
|
||||||
|
onResult(
|
||||||
|
VaultLinkOutcome.Failed(
|
||||||
|
"YandexSignInService: call registerWith(activity) from MainActivity.onCreate first",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
synchronized(this) { pending = onResult }
|
||||||
l.launch(YandexAuthLoginOptions(LoginType.WEBVIEW))
|
l.launch(YandexAuthLoginOptions(LoginType.WEBVIEW))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapYandexResult(result: YandexAuthResult): RemoteYandexAuthResult = when (result) {
|
private fun mapYandexResult(result: YandexAuthResult): VaultLinkOutcome = when (result) {
|
||||||
is YandexAuthResult.Success ->
|
is YandexAuthResult.Success ->
|
||||||
RemoteYandexAuthResult.Success(result.token.value)
|
VaultLinkOutcome.Success(YandexRegistration(result.token.value))
|
||||||
is YandexAuthResult.Failure ->
|
is YandexAuthResult.Failure ->
|
||||||
RemoteYandexAuthResult.Failure(
|
VaultLinkOutcome.Failed(
|
||||||
result.exception.message ?: result.exception.toString(),
|
result.exception.message ?: result.exception.toString(),
|
||||||
)
|
)
|
||||||
YandexAuthResult.Cancelled -> RemoteYandexAuthResult.Cancelled
|
YandexAuthResult.Cancelled -> VaultLinkOutcome.Cancelled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.github.nullptroma.wallenc.app.di.modules.auth
|
package com.github.nullptroma.wallenc.app.di.modules.auth
|
||||||
|
|
||||||
import com.github.nullptroma.wallenc.app.auth.YandexSignInService
|
import com.github.nullptroma.wallenc.app.auth.YandexSignInService
|
||||||
import com.github.nullptroma.wallenc.domain.auth.RemoteYandexSignInLauncher
|
import com.github.nullptroma.wallenc.vaultapi.RemoteVaultAuthenticator
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -14,7 +14,7 @@ abstract class YandexAuthModule {
|
|||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun bindRemoteYandexSignInLauncher(
|
abstract fun bindRemoteVaultAuthenticator(
|
||||||
impl: YandexSignInService,
|
impl: YandexSignInService,
|
||||||
): RemoteYandexSignInLauncher
|
): RemoteVaultAuthenticator
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.github.nullptroma.wallenc.data.vaults.VaultsManager
|
|||||||
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 com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||||
|
import com.github.nullptroma.wallenc.vaultapi.VaultRegistrar
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -45,7 +46,7 @@ class SingletonModule {
|
|||||||
keyRepo: StorageKeyMapRepository,
|
keyRepo: StorageKeyMapRepository,
|
||||||
yandexAccountRepository: YandexAccountRepository,
|
yandexAccountRepository: YandexAccountRepository,
|
||||||
yandexUserInfoRepository: YandexUserInfoRepository,
|
yandexUserInfoRepository: YandexUserInfoRepository,
|
||||||
): IVaultsManager {
|
): VaultsManager {
|
||||||
return VaultsManager(
|
return VaultsManager(
|
||||||
ioDispatcher = ioDispatcher,
|
ioDispatcher = ioDispatcher,
|
||||||
context = context,
|
context = context,
|
||||||
@@ -55,6 +56,14 @@ class SingletonModule {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideIVaultsManager(impl: VaultsManager): IVaultsManager = impl
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideVaultRegistrar(impl: VaultsManager): VaultRegistrar = impl
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideUnlockManager(
|
fun provideUnlockManager(
|
||||||
vaultsManager: IVaultsManager
|
vaultsManager: IVaultsManager
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package com.github.nullptroma.wallenc.app.di.modules.domain
|
|||||||
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 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.ManageStoragesEncryptionUseCase
|
import com.github.nullptroma.wallenc.domain.usecases.ManageStoragesEncryptionUseCase
|
||||||
|
import com.github.nullptroma.wallenc.domain.usecases.ManageVaultUseCase
|
||||||
import com.github.nullptroma.wallenc.domain.usecases.RemoveStorageUseCase
|
import com.github.nullptroma.wallenc.domain.usecases.RemoveStorageUseCase
|
||||||
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
|
||||||
@@ -25,8 +25,8 @@ class UseCasesModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideManageLocalVaultUseCase(vaultsManager: IVaultsManager): ManageLocalVaultUseCase {
|
fun provideManageVaultUseCase(vaultsManager: IVaultsManager): ManageVaultUseCase {
|
||||||
return ManageLocalVaultUseCase(vaultsManager)
|
return ManageVaultUseCase(vaultsManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
@@ -54,4 +54,5 @@ dependencies {
|
|||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|
||||||
implementation(project(":domain"))
|
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.db.app.repository.YandexAccountRepository
|
||||||
import com.github.nullptroma.wallenc.data.network.yandexuserinfo.repository.YandexUserInfoRepository
|
import com.github.nullptroma.wallenc.data.network.yandexuserinfo.repository.YandexUserInfoRepository
|
||||||
import com.github.nullptroma.wallenc.data.storages.UnlockManager
|
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.IStorage
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
|
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 com.github.nullptroma.wallenc.vaultapi.VaultRegistrar
|
||||||
|
import com.github.nullptroma.wallenc.vaultapi.VaultRegistration
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -26,21 +32,17 @@ class VaultsManager(
|
|||||||
keyRepo: StorageKeyMapRepository,
|
keyRepo: StorageKeyMapRepository,
|
||||||
private val yandexAccountRepository: YandexAccountRepository,
|
private val yandexAccountRepository: YandexAccountRepository,
|
||||||
private val yandexUserInfoRepository: YandexUserInfoRepository,
|
private val yandexUserInfoRepository: YandexUserInfoRepository,
|
||||||
) : IVaultsManager {
|
) : IVaultsManager, VaultRegistrar {
|
||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + ioDispatcher)
|
private val scope = CoroutineScope(SupervisorJob() + ioDispatcher)
|
||||||
|
|
||||||
override val localVault = LocalVault(ioDispatcher, context)
|
private val localVault: IVault = LocalVault(
|
||||||
|
|
||||||
// До unlockManager: UnlockManager в init подписывается на allStorages.
|
|
||||||
override val allStorages: StateFlow<List<IStorage>> = localVault.storages
|
|
||||||
|
|
||||||
override val unlockManager: IUnlockManager = UnlockManager(
|
|
||||||
keymapRepository = keyRepo,
|
|
||||||
ioDispatcher = ioDispatcher,
|
ioDispatcher = ioDispatcher,
|
||||||
vaultsManager = this
|
context = context,
|
||||||
|
idStore = LocalVaultIdStore(context),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val _remoteVaults = yandexAccountRepository.observeAll()
|
private val yandexVaults: StateFlow<List<IVault>> = yandexAccountRepository.observeAll()
|
||||||
.map { rows ->
|
.map { rows ->
|
||||||
rows.map { row ->
|
rows.map { row ->
|
||||||
YandexVault(
|
YandexVault(
|
||||||
@@ -52,33 +54,51 @@ class VaultsManager(
|
|||||||
}
|
}
|
||||||
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
override val remoteVaults: StateFlow<List<IVault>> = _remoteVaults
|
override val vaults: StateFlow<List<IVault>> = yandexVaults
|
||||||
|
.map { remote -> listOf(localVault) + remote }
|
||||||
override val allVaults: StateFlow<List<IVault>> = _remoteVaults
|
|
||||||
.map { listOf(localVault) + it }
|
|
||||||
.stateIn(scope, SharingStarted.Eagerly, listOf(localVault))
|
.stateIn(scope, SharingStarted.Eagerly, listOf(localVault))
|
||||||
|
|
||||||
override suspend fun addYandexVault(accessToken: String) = withContext(ioDispatcher) {
|
// Поведение Phase 1 — UnlockManager работает только с локальными storages.
|
||||||
val info = yandexUserInfoRepository.userInfo(accessToken)
|
// Расширение до 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() }
|
val email = info.defaultEmail?.takeIf { it.isNotBlank() }
|
||||||
?: "${info.login}@yandex.ru"
|
?: "${info.login}@yandex.ru"
|
||||||
val existing = yandexAccountRepository.getByYandexUserId(info.id)
|
val existing = yandexAccountRepository.getByYandexUserId(info.id)
|
||||||
val vaultUuid = existing?.vaultUuid ?: UUID.randomUUID().toString()
|
val vaultUuid = existing?.vaultUuid ?: UUID.randomUUID().toString()
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
yandexAccountRepository.updateCredentials(vaultUuid, email, accessToken)
|
yandexAccountRepository.updateCredentials(vaultUuid, email, token)
|
||||||
} else {
|
} else {
|
||||||
yandexAccountRepository.insert(
|
yandexAccountRepository.insert(
|
||||||
DbYandexAccount(
|
DbYandexAccount(
|
||||||
vaultUuid = vaultUuid,
|
vaultUuid = vaultUuid,
|
||||||
yandexUserId = info.id,
|
yandexUserId = info.id,
|
||||||
email = email,
|
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 android.content.Context
|
||||||
import com.github.nullptroma.wallenc.data.storages.local.LocalStorage
|
import com.github.nullptroma.wallenc.data.storages.local.LocalStorage
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
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.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.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -18,21 +18,26 @@ import kotlin.io.path.Path
|
|||||||
import kotlin.io.path.createDirectory
|
import kotlin.io.path.createDirectory
|
||||||
import kotlin.io.path.pathString
|
import kotlin.io.path.pathString
|
||||||
|
|
||||||
class LocalVault(private val ioDispatcher: CoroutineDispatcher, context: Context) : IVault {
|
class LocalVault(
|
||||||
override val type: VaultType = VaultType.LOCAL
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
override val uuid: UUID
|
context: Context,
|
||||||
get() = TODO("Not yet implemented")
|
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
|
override val storages: StateFlow<List<IStorage>> = _storages
|
||||||
|
|
||||||
private val _isAvailable = MutableStateFlow(false)
|
private val _isAvailable = MutableStateFlow(false)
|
||||||
override val isAvailable: StateFlow<Boolean> = _isAvailable
|
override val isAvailable: StateFlow<Boolean> = _isAvailable
|
||||||
|
|
||||||
private val _totalSpace = MutableStateFlow(null)
|
private val _totalSpace = MutableStateFlow<Int?>(null)
|
||||||
override val totalSpace: StateFlow<Int?> = _totalSpace
|
override val totalSpace: StateFlow<Int?> = _totalSpace
|
||||||
|
|
||||||
private val _availableSpace = MutableStateFlow(null)
|
private val _availableSpace = MutableStateFlow<Int?>(null)
|
||||||
override val availableSpace: StateFlow<Int?> = _availableSpace
|
override val availableSpace: StateFlow<Int?> = _availableSpace
|
||||||
|
|
||||||
private val path = MutableStateFlow<File?>(null)
|
private val path = MutableStateFlow<File?>(null)
|
||||||
@@ -54,7 +59,7 @@ class LocalVault(private val ioDispatcher: CoroutineDispatcher, context: Context
|
|||||||
if (dirs != null) {
|
if (dirs != null) {
|
||||||
_storages.value = dirs.map {
|
_storages.value = dirs.map {
|
||||||
val uuid = UUID.fromString(it.name)
|
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(
|
override suspend fun createStorage(
|
||||||
enc: StorageEncryptionInfo
|
enc: StorageEncryptionInfo,
|
||||||
): LocalStorage = withContext(ioDispatcher) {
|
): LocalStorage = withContext(ioDispatcher) {
|
||||||
val storage = createStorage()
|
val storage = createStorage()
|
||||||
storage.setEncInfo(enc)
|
storage.setEncInfo(enc)
|
||||||
@@ -90,8 +95,8 @@ class LocalVault(private val ioDispatcher: CoroutineDispatcher, context: Context
|
|||||||
|
|
||||||
val curStorages = _storages.value.toMutableList()
|
val curStorages = _storages.value.toMutableList()
|
||||||
val index = curStorages.indexOfFirst { it.uuid == storage.uuid }
|
val index = curStorages.indexOfFirst { it.uuid == storage.uuid }
|
||||||
if(index != -1) {
|
if (index != -1) {
|
||||||
val localStorage = curStorages[index]
|
val localStorage = curStorages[index] as LocalStorage
|
||||||
curStorages.removeAt(index)
|
curStorages.removeAt(index)
|
||||||
_storages.value = curStorages
|
_storages.value = curStorages
|
||||||
File(localStorage.absolutePath).deleteRecursively()
|
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 Диска.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.auth
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Результат входа через Яндекс (без зависимости от Yandex SDK).
|
|
||||||
*/
|
|
||||||
sealed interface RemoteYandexAuthResult {
|
|
||||||
data class Success(val accessToken: String) : RemoteYandexAuthResult
|
|
||||||
data class Failure(val message: String) : RemoteYandexAuthResult
|
|
||||||
data object Cancelled : RemoteYandexAuthResult
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Запуск OAuth Яндекса. Реализация только в модуле app (Yandex Auth SDK).
|
|
||||||
*/
|
|
||||||
fun interface RemoteYandexSignInLauncher {
|
|
||||||
fun launch(onResult: (RemoteYandexAuthResult) -> Unit)
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.enums
|
|
||||||
|
|
||||||
enum class VaultType {
|
|
||||||
LOCAL,
|
|
||||||
DECRYPTED,
|
|
||||||
YANDEX
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,16 @@ 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 kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Контракт vault'а: коллекция [IStorage] с реактивным состоянием.
|
||||||
|
*
|
||||||
|
* domain не различает локальные/удалённые/Yandex/etc. — это общий порт.
|
||||||
|
*/
|
||||||
interface IVault : IVaultInfo {
|
interface IVault : IVaultInfo {
|
||||||
override val storages: StateFlow<List<IStorage>?>
|
val storages: StateFlow<List<IStorage>>
|
||||||
|
val isAvailable: StateFlow<Boolean>
|
||||||
|
val totalSpace: StateFlow<Int?>
|
||||||
|
val availableSpace: StateFlow<Int?>
|
||||||
|
|
||||||
suspend fun createStorage(): IStorage
|
suspend fun createStorage(): IStorage
|
||||||
suspend fun createStorage(enc: StorageEncryptionInfo): IStorage
|
suspend fun createStorage(enc: StorageEncryptionInfo): IStorage
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.interfaces
|
package com.github.nullptroma.wallenc.domain.interfaces
|
||||||
|
|
||||||
import com.github.nullptroma.wallenc.domain.enums.VaultType
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
sealed interface IVaultInfo {
|
/**
|
||||||
val type: VaultType
|
* Минимальная идентификация vault'а.
|
||||||
|
*
|
||||||
|
* Намеренно «голая»: domain ничего не знает о брендах, локальности или статусе —
|
||||||
|
* вся категоризация лежит во внешнем кольце (`:vault-api: VaultDescriptor`).
|
||||||
|
*/
|
||||||
|
interface IVaultInfo {
|
||||||
val uuid: UUID
|
val uuid: UUID
|
||||||
val storages: StateFlow<List<IStorageInfo>?>
|
|
||||||
val isAvailable: StateFlow<Boolean>
|
|
||||||
val totalSpace: StateFlow<Int?>
|
|
||||||
val availableSpace: StateFlow<Int?>
|
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.interfaces
|
package com.github.nullptroma.wallenc.domain.interfaces
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Единая точка доступа ко всем vault'ам приложения.
|
||||||
|
*
|
||||||
|
* domain не различает категории vault'ов — потребители (presentation) фильтруют
|
||||||
|
* [vaults] через `:vault-api` (`VaultDescriptor`/`DescribedVault`).
|
||||||
|
*/
|
||||||
interface IVaultsManager {
|
interface IVaultsManager {
|
||||||
val localVault: IVault
|
val vaults: StateFlow<List<IVault>>
|
||||||
val unlockManager: IUnlockManager
|
|
||||||
val remoteVaults: StateFlow<List<IVault>>
|
|
||||||
val allStorages: StateFlow<List<IStorage>>
|
val allStorages: StateFlow<List<IStorage>>
|
||||||
val allVaults: StateFlow<List<IVault>>
|
val unlockManager: IUnlockManager
|
||||||
suspend fun addYandexVault(accessToken: String)
|
|
||||||
|
|
||||||
suspend fun removeRemoteVault(vaultUuid: UUID)
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.interfaces
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Удалённый vault Яндекс с привязанным аккаунтом (почта для UI).
|
|
||||||
*/
|
|
||||||
interface IYandexVault : IVault {
|
|
||||||
val accountEmail: String
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.github.nullptroma.wallenc.domain.usecases
|
|
||||||
|
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
|
|
||||||
class ManageLocalVaultUseCase(private val manager: IVaultsManager) {
|
|
||||||
val localStorages: StateFlow<List<IStorageInfo>?>
|
|
||||||
get() = manager.localVault.storages
|
|
||||||
|
|
||||||
suspend fun createStorage() {
|
|
||||||
manager.localVault.createStorage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.github.nullptroma.wallenc.domain.usecases
|
||||||
|
|
||||||
|
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.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class ManageVaultUseCase(private val manager: IVaultsManager) {
|
||||||
|
|
||||||
|
/** Найти vault по идентификатору в текущем состоянии. */
|
||||||
|
fun find(vaultUuid: UUID): IVault? =
|
||||||
|
manager.vaults.value.firstOrNull { it.uuid == vaultUuid }
|
||||||
|
|
||||||
|
/** Реактивно отдаёт сам vault, либо null если он отсутствует. */
|
||||||
|
fun observe(vaultUuid: UUID): Flow<IVault?> =
|
||||||
|
manager.vaults.map { list -> list.firstOrNull { it.uuid == vaultUuid } }
|
||||||
|
|
||||||
|
/** Реактивно отдаёт storages указанного vault'а; пустой список, если vault не найден. */
|
||||||
|
fun storagesOf(vaultUuid: UUID): Flow<List<IStorage>> =
|
||||||
|
observe(vaultUuid).flatMapLatest { vault -> vault?.storages ?: flowOf(emptyList()) }
|
||||||
|
|
||||||
|
/** Создать новое хранилище в указанном vault'е. */
|
||||||
|
suspend fun createStorage(vaultUuid: UUID): IStorage {
|
||||||
|
val vault = find(vaultUuid)
|
||||||
|
?: throw IllegalStateException("Vault $vaultUuid is not registered")
|
||||||
|
return vault.createStorage()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ package com.github.nullptroma.wallenc.domain.usecases
|
|||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
|
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.domain.interfaces.IVaultsManager
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class RemoveStorageUseCase(
|
class RemoveStorageUseCase(
|
||||||
private val vaultsManager: IVaultsManager,
|
private val vaultsManager: IVaultsManager,
|
||||||
@@ -12,12 +14,11 @@ class RemoveStorageUseCase(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun remove(storage: IStorageInfo) {
|
suspend fun remove(storage: IStorageInfo) {
|
||||||
|
|
||||||
if (storage !is IStorage) return
|
if (storage !is IStorage) return
|
||||||
|
|
||||||
if (!storage.isVirtualStorage) {
|
if (!storage.isVirtualStorage) {
|
||||||
unlockManager.close(storage)
|
unlockManager.close(storage)
|
||||||
vaultsManager.localVault.remove(storage)
|
findOwningVault(storage.uuid)?.remove(storage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,13 +26,18 @@ class RemoveStorageUseCase(
|
|||||||
manageStoragesEncryptionUseCase.clearAndDisableEncryption(parent)
|
manageStoragesEncryptionUseCase.clearAndDisableEncryption(parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun findOwningVault(storageUuid: UUID): IVault? =
|
||||||
|
vaultsManager.vaults.value.firstOrNull { v ->
|
||||||
|
v.storages.value.any { it.uuid == storageUuid }
|
||||||
|
}
|
||||||
|
|
||||||
private fun findParentStorage(storage: IStorage): IStorage? {
|
private fun findParentStorage(storage: IStorage): IStorage? {
|
||||||
val opened = unlockManager.openedStorages.value
|
val opened = unlockManager.openedStorages.value
|
||||||
val parentUuid = opened.entries.firstOrNull { it.value.uuid == storage.uuid }?.key ?: return null
|
val parentUuid = opened.entries.firstOrNull { it.value.uuid == storage.uuid }?.key
|
||||||
val locals = vaultsManager.localVault.storages.value.orEmpty()
|
?: return null
|
||||||
return locals.firstOrNull { it.uuid == parentUuid }
|
val realParent = vaultsManager.vaults.value
|
||||||
?: opened[parentUuid]
|
.flatMap { it.storages.value }
|
||||||
|
.firstOrNull { it.uuid == parentUuid }
|
||||||
|
return realParent ?: opened[parentUuid]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,4 +79,5 @@ dependencies {
|
|||||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||||
|
|
||||||
implementation(project(":domain"))
|
implementation(project(":domain"))
|
||||||
|
implementation(project(":vault-api"))
|
||||||
}
|
}
|
||||||
@@ -7,29 +7,38 @@ 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.ILogger
|
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.interfaces.IVaultsManager
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||||
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.ManageStoragesEncryptionUseCase
|
import com.github.nullptroma.wallenc.domain.usecases.ManageStoragesEncryptionUseCase
|
||||||
|
import com.github.nullptroma.wallenc.domain.usecases.ManageVaultUseCase
|
||||||
import com.github.nullptroma.wallenc.domain.usecases.RemoveStorageUseCase
|
import com.github.nullptroma.wallenc.domain.usecases.RemoveStorageUseCase
|
||||||
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.ViewModelBase
|
import com.github.nullptroma.wallenc.presentation.ViewModelBase
|
||||||
import com.github.nullptroma.wallenc.presentation.extensions.toPrintable
|
import com.github.nullptroma.wallenc.presentation.extensions.toPrintable
|
||||||
|
import com.github.nullptroma.wallenc.vaultapi.described
|
||||||
|
import com.github.nullptroma.wallenc.vaultapi.locals
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LocalVaultViewModel @Inject constructor(
|
class LocalVaultViewModel @Inject constructor(
|
||||||
private val manageLocalVaultUseCase: ManageLocalVaultUseCase,
|
private val vaultsManager: IVaultsManager,
|
||||||
|
private val manageVaultUseCase: ManageVaultUseCase,
|
||||||
private val removeStorageUseCase: RemoveStorageUseCase,
|
private val removeStorageUseCase: RemoveStorageUseCase,
|
||||||
private val getOpenedStoragesUseCase: GetOpenedStoragesUseCase,
|
private val getOpenedStoragesUseCase: GetOpenedStoragesUseCase,
|
||||||
private val storageFileManagementUseCase: StorageFileManagementUseCase,
|
private val storageFileManagementUseCase: StorageFileManagementUseCase,
|
||||||
@@ -38,6 +47,13 @@ class LocalVaultViewModel @Inject constructor(
|
|||||||
private val taskOrchestrator: ITaskOrchestrator,
|
private val taskOrchestrator: ITaskOrchestrator,
|
||||||
private val logger: ILogger
|
private val logger: ILogger
|
||||||
) : ViewModelBase<LocalVaultScreenState>(LocalVaultScreenState(listOf(), true)) {
|
) : ViewModelBase<LocalVaultScreenState>(LocalVaultScreenState(listOf(), true)) {
|
||||||
|
|
||||||
|
private val localVaultUuid: UUID?
|
||||||
|
get() = vaultsManager.vaults.value.described().locals.firstOrNull()?.uuid
|
||||||
|
|
||||||
|
private val localStoragesFlow = vaultsManager.vaults
|
||||||
|
.map { vaults -> vaults.described().locals.firstOrNull() }
|
||||||
|
.flatMapLatest { v -> v?.storages ?: flowOf(emptyList()) }
|
||||||
private val _messages = MutableSharedFlow<String>()
|
private val _messages = MutableSharedFlow<String>()
|
||||||
val messages: SharedFlow<String> = _messages
|
val messages: SharedFlow<String> = _messages
|
||||||
|
|
||||||
@@ -69,24 +85,21 @@ class LocalVaultViewModel @Inject constructor(
|
|||||||
|
|
||||||
private fun collectFlows() {
|
private fun collectFlows() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
manageLocalVaultUseCase.localStorages.combine(getOpenedStoragesUseCase.openedStorages) { local, opened ->
|
localStoragesFlow.combine(getOpenedStoragesUseCase.openedStorages) { local, opened ->
|
||||||
if (local == null) {
|
|
||||||
return@combine Pair(true, emptyList<Tree<IStorageInfo>>())
|
|
||||||
}
|
|
||||||
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<IStorageInfo>(storage)
|
||||||
list.add(tree)
|
list.add(tree)
|
||||||
while (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<IStorageInfo>(child)
|
||||||
tree.children = listOf(nextTree)
|
tree.children = listOf(nextTree)
|
||||||
tree = nextTree
|
tree = nextTree
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@combine Pair(false, list)
|
list
|
||||||
}.collect { (loading, trees) ->
|
}.collect { trees ->
|
||||||
isLoading = loading
|
isLoading = false
|
||||||
updateState(state.value.copy(storagesList = trees))
|
updateState(state.value.copy(storagesList = trees))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,7 +140,9 @@ class LocalVaultViewModel @Inject constructor(
|
|||||||
dispatcher = Dispatchers.IO,
|
dispatcher = Dispatchers.IO,
|
||||||
work = { ctx ->
|
work = { ctx ->
|
||||||
ctx.log(TaskLogLevel.Info, "Creating storage…")
|
ctx.log(TaskLogLevel.Info, "Creating storage…")
|
||||||
manageLocalVaultUseCase.createStorage()
|
val uuid = localVaultUuid
|
||||||
|
?: throw IllegalStateException("Local vault is not registered")
|
||||||
|
manageVaultUseCase.createStorage(uuid)
|
||||||
ctx.log(TaskLogLevel.Info, "Storage created")
|
ctx.log(TaskLogLevel.Info, "Storage created")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
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.auth.RemoteYandexAuthResult
|
|
||||||
import com.github.nullptroma.wallenc.domain.enums.VaultType
|
|
||||||
import com.github.nullptroma.wallenc.presentation.R
|
import com.github.nullptroma.wallenc.presentation.R
|
||||||
|
import com.github.nullptroma.wallenc.vaultapi.CloudBrand
|
||||||
|
import com.github.nullptroma.wallenc.vaultapi.VaultLinkOutcome
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RemoteVaultsScreen(
|
fun RemoteVaultsScreen(
|
||||||
@@ -114,10 +114,9 @@ fun RemoteVaultsScreen(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = when (item.type) {
|
text = when (item.brand) {
|
||||||
VaultType.YANDEX ->
|
CloudBrand.YANDEX ->
|
||||||
stringResource(R.string.remote_vault_type_yandex)
|
stringResource(R.string.remote_vault_type_yandex)
|
||||||
else -> item.type.name
|
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -172,14 +171,14 @@ fun RemoteVaultsScreen(
|
|||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.setAddChoiceVisible(false)
|
viewModel.setAddChoiceVisible(false)
|
||||||
viewModel.yandexSignIn.launch { outcome ->
|
viewModel.remoteAuthenticator.beginLink(CloudBrand.YANDEX) { outcome ->
|
||||||
when (outcome) {
|
when (outcome) {
|
||||||
is RemoteYandexAuthResult.Success ->
|
is VaultLinkOutcome.Success ->
|
||||||
viewModel.onYandexAuthSuccess(outcome.accessToken)
|
viewModel.onLinkSucceeded(outcome.registration)
|
||||||
is RemoteYandexAuthResult.Failure ->
|
is VaultLinkOutcome.Failed ->
|
||||||
Toast.makeText(context, outcome.message, Toast.LENGTH_LONG)
|
Toast.makeText(context, outcome.message, Toast.LENGTH_LONG)
|
||||||
.show()
|
.show()
|
||||||
RemoteYandexAuthResult.Cancelled -> { }
|
VaultLinkOutcome.Cancelled -> { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.github.nullptroma.wallenc.presentation.screens.main.screens.remotes
|
package com.github.nullptroma.wallenc.presentation.screens.main.screens.remotes
|
||||||
|
|
||||||
import com.github.nullptroma.wallenc.domain.enums.VaultType
|
import com.github.nullptroma.wallenc.vaultapi.CloudBrand
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
data class RemoteVaultListItem(
|
data class RemoteVaultListItem(
|
||||||
val uuid: UUID,
|
val uuid: UUID,
|
||||||
val type: VaultType,
|
val brand: CloudBrand,
|
||||||
val label: String,
|
val label: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +13,5 @@ data class RemoteVaultsScreenState(
|
|||||||
val vaults: List<RemoteVaultListItem> = emptyList(),
|
val vaults: List<RemoteVaultListItem> = emptyList(),
|
||||||
val isBusy: Boolean = false,
|
val isBusy: Boolean = false,
|
||||||
val addChoiceVisible: Boolean = false,
|
val addChoiceVisible: Boolean = false,
|
||||||
/** Карточка, для которой показан диалог удаления */
|
|
||||||
val vaultPendingDelete: RemoteVaultListItem? = null,
|
val vaultPendingDelete: RemoteVaultListItem? = null,
|
||||||
)
|
)
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
package com.github.nullptroma.wallenc.presentation.screens.main.screens.remotes
|
package com.github.nullptroma.wallenc.presentation.screens.main.screens.remotes
|
||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.nullptroma.wallenc.domain.auth.RemoteYandexSignInLauncher
|
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IYandexVault
|
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
|
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||||
import com.github.nullptroma.wallenc.presentation.ViewModelBase
|
import com.github.nullptroma.wallenc.presentation.ViewModelBase
|
||||||
|
import com.github.nullptroma.wallenc.vaultapi.RemoteVaultAuthenticator
|
||||||
|
import com.github.nullptroma.wallenc.vaultapi.VaultDescriptor
|
||||||
|
import com.github.nullptroma.wallenc.vaultapi.VaultRegistrar
|
||||||
|
import com.github.nullptroma.wallenc.vaultapi.VaultRegistration
|
||||||
|
import com.github.nullptroma.wallenc.vaultapi.described
|
||||||
|
import com.github.nullptroma.wallenc.vaultapi.remotes
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
@@ -18,24 +22,22 @@ import javax.inject.Inject
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class RemoteVaultsViewModel @Inject constructor(
|
class RemoteVaultsViewModel @Inject constructor(
|
||||||
private val vaultsManager: IVaultsManager,
|
private val vaultsManager: IVaultsManager,
|
||||||
val yandexSignIn: RemoteYandexSignInLauncher,
|
private val vaultRegistrar: VaultRegistrar,
|
||||||
|
val remoteAuthenticator: RemoteVaultAuthenticator,
|
||||||
private val taskOrchestrator: ITaskOrchestrator,
|
private val taskOrchestrator: ITaskOrchestrator,
|
||||||
) : ViewModelBase<RemoteVaultsScreenState>(RemoteVaultsScreenState()) {
|
) : ViewModelBase<RemoteVaultsScreenState>(RemoteVaultsScreenState()) {
|
||||||
|
|
||||||
val uiState = combine(
|
val uiState = combine(
|
||||||
vaultsManager.remoteVaults,
|
vaultsManager.vaults,
|
||||||
state,
|
state,
|
||||||
) { remotes, base ->
|
) { all, base ->
|
||||||
base.copy(
|
base.copy(
|
||||||
vaults = remotes.map { v ->
|
vaults = all.described().remotes.mapNotNull { v ->
|
||||||
val label = when (v) {
|
val descriptor = v.descriptor as? VaultDescriptor.LinkedRemote ?: return@mapNotNull null
|
||||||
is IYandexVault -> v.accountEmail
|
|
||||||
else -> v.uuid.toString()
|
|
||||||
}
|
|
||||||
RemoteVaultListItem(
|
RemoteVaultListItem(
|
||||||
uuid = v.uuid,
|
uuid = descriptor.uuid,
|
||||||
type = v.type,
|
brand = descriptor.brand,
|
||||||
label = label,
|
label = descriptor.accountDisplayName,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -53,15 +55,15 @@ class RemoteVaultsViewModel @Inject constructor(
|
|||||||
updateState(state.value.copy(isBusy = busy))
|
updateState(state.value.copy(isBusy = busy))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onYandexAuthSuccess(accessToken: String) {
|
fun onLinkSucceeded(registration: VaultRegistration) {
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
taskOrchestrator.enqueue(
|
taskOrchestrator.enqueue(
|
||||||
title = "Add Yandex vault",
|
title = "Add remote vault",
|
||||||
dispatcher = Dispatchers.IO,
|
dispatcher = Dispatchers.IO,
|
||||||
work = { ctx ->
|
work = { ctx ->
|
||||||
try {
|
try {
|
||||||
ctx.log(TaskLogLevel.Info, "Adding vault…")
|
ctx.log(TaskLogLevel.Info, "Adding vault…")
|
||||||
vaultsManager.addYandexVault(accessToken)
|
vaultRegistrar.register(registration)
|
||||||
ctx.log(TaskLogLevel.Info, "Vault added")
|
ctx.log(TaskLogLevel.Info, "Vault added")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to add vault")
|
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to add vault")
|
||||||
@@ -93,7 +95,7 @@ class RemoteVaultsViewModel @Inject constructor(
|
|||||||
work = { ctx ->
|
work = { ctx ->
|
||||||
try {
|
try {
|
||||||
ctx.log(TaskLogLevel.Info, "Removing remote vault…")
|
ctx.log(TaskLogLevel.Info, "Removing remote vault…")
|
||||||
vaultsManager.removeRemoteVault(uuid)
|
vaultRegistrar.unregister(uuid)
|
||||||
ctx.log(TaskLogLevel.Info, "Remote vault removed")
|
ctx.log(TaskLogLevel.Info, "Remote vault removed")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to remove vault")
|
ctx.log(TaskLogLevel.Error, e.message ?: "Failed to remove vault")
|
||||||
|
|||||||
@@ -27,3 +27,4 @@ include(":app")
|
|||||||
include(":data")
|
include(":data")
|
||||||
include(":domain")
|
include(":domain")
|
||||||
include(":presentation")
|
include(":presentation")
|
||||||
|
include(":vault-api")
|
||||||
|
|||||||
23
vault-api/build.gradle.kts
Normal file
23
vault-api/build.gradle.kts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import org.gradle.api.file.DuplicatesStrategy
|
||||||
|
import org.gradle.api.tasks.bundling.Jar
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("java-library")
|
||||||
|
alias(libs.plugins.jetbrains.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Jar>().configureEach {
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":domain"))
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.github.nullptroma.wallenc.vaultapi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Поддерживаемые облачные провайдеры удалённых vault'ов.
|
||||||
|
*
|
||||||
|
* Бренд «локального» устройства тут не указывается — для дискриминации UI
|
||||||
|
* используется [VaultDescriptor.LocalDevice] vs [VaultDescriptor.LinkedRemote].
|
||||||
|
*/
|
||||||
|
enum class CloudBrand {
|
||||||
|
YANDEX,
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.github.nullptroma.wallenc.vaultapi
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.interfaces.IVault
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vault, дополнительно знающий свою категорию для UI/маршрутизации.
|
||||||
|
*
|
||||||
|
* Все реализации vault'ов в `:data` обязаны быть [DescribedVault] —
|
||||||
|
* это единственный способ для presentation/app узнать, что это за vault,
|
||||||
|
* не имея зависимости от `:data`.
|
||||||
|
*/
|
||||||
|
interface DescribedVault : IVault {
|
||||||
|
val descriptor: VaultDescriptor
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.github.nullptroma.wallenc.vaultapi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запуск OAuth-сценария привязки удалённого vault'а для конкретного [CloudBrand].
|
||||||
|
*
|
||||||
|
* Реализация в `:app` (привязана к Activity). presentation вызывает [beginLink]
|
||||||
|
* из контекста Compose-экрана и получает [VaultLinkOutcome] асинхронно.
|
||||||
|
*
|
||||||
|
* Намеренно императивный API через колбэк, чтобы корректно встроиться
|
||||||
|
* в `ActivityResultLauncher` Yandex Auth SDK без удержания Activity синглтоном.
|
||||||
|
*/
|
||||||
|
fun interface RemoteVaultAuthenticator {
|
||||||
|
/**
|
||||||
|
* Начать сценарий линка для бренда [brand].
|
||||||
|
*
|
||||||
|
* Если бренд не поддерживается — реализация должна синхронно вызвать
|
||||||
|
* `onResult(VaultLinkOutcome.Failed(...))`, не бросая исключение.
|
||||||
|
*/
|
||||||
|
fun beginLink(brand: CloudBrand, onResult: (VaultLinkOutcome) -> Unit)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.github.nullptroma.wallenc.vaultapi
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.interfaces.IVaultInfo
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Категория vault'а для UI и ветвления — sealed-иерархия в одном модуле,
|
||||||
|
* чтобы `when` был exhaustive.
|
||||||
|
*
|
||||||
|
* `domain` про эти подтипы ничего не знает; внешнее кольцо (`:vault-api`)
|
||||||
|
* расширяет [IVaultInfo] (только `uuid`) до структуры с дополнительной
|
||||||
|
* категоризацией.
|
||||||
|
*/
|
||||||
|
sealed interface VaultDescriptor : IVaultInfo {
|
||||||
|
/** Vault, физически живущий на устройстве; без привязки к облачному аккаунту. */
|
||||||
|
data class LocalDevice(
|
||||||
|
override val uuid: UUID,
|
||||||
|
) : VaultDescriptor
|
||||||
|
|
||||||
|
/** Vault, привязанный к облачному аккаунту через OAuth. */
|
||||||
|
data class LinkedRemote(
|
||||||
|
override val uuid: UUID,
|
||||||
|
val brand: CloudBrand,
|
||||||
|
val accountDisplayName: String,
|
||||||
|
) : VaultDescriptor
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.github.nullptroma.wallenc.vaultapi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Результат сценария OAuth-линка нового удалённого vault'а.
|
||||||
|
*
|
||||||
|
* presentation сводит это к: успех → отдать `registration` в [VaultRegistrar.register];
|
||||||
|
* cancel → ничего не делать; failure → показать сообщение.
|
||||||
|
*/
|
||||||
|
sealed interface VaultLinkOutcome {
|
||||||
|
/** Пользователь успешно вошёл; в [registration] лежат данные для регистрации. */
|
||||||
|
data class Success(val registration: VaultRegistration) : VaultLinkOutcome
|
||||||
|
|
||||||
|
/** Пользователь отменил вход. */
|
||||||
|
data object Cancelled : VaultLinkOutcome
|
||||||
|
|
||||||
|
/** Ошибка SDK/сети/сервера; [message] годится для показа пользователю. */
|
||||||
|
data class Failed(val message: String) : VaultLinkOutcome
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.github.nullptroma.wallenc.vaultapi
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Контракт регистрации/удаления удалённого vault'а.
|
||||||
|
* Реализуется тем же объектом, что и `domain.IVaultsManager`.
|
||||||
|
*/
|
||||||
|
interface VaultRegistrar {
|
||||||
|
/**
|
||||||
|
* Зарегистрировать новый удалённый vault по «полезной нагрузке» из
|
||||||
|
* [VaultLinkOutcome.Success]. Если тип [registration] неизвестен реализации,
|
||||||
|
* бросает [IllegalArgumentException].
|
||||||
|
*/
|
||||||
|
suspend fun register(registration: VaultRegistration)
|
||||||
|
|
||||||
|
/** Удалить ранее зарегистрированный vault по идентификатору. */
|
||||||
|
suspend fun unregister(vaultUuid: UUID)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.github.nullptroma.wallenc.vaultapi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Маркер «полезной нагрузки» для регистрации удалённого vault'а через [VaultRegistrar].
|
||||||
|
*
|
||||||
|
* Намеренно НЕ sealed: конкретные реализации (`YandexRegistration`, …) живут в `:data`
|
||||||
|
* рядом с соответствующими реализациями vault'а, чтобы `:data` не разнесёшь по
|
||||||
|
* нескольким модулям без необходимости. Цена — отсутствие exhaustive-when через
|
||||||
|
* границу модуля, лечится fail-fast веткой `else` в `VaultsManager.register(...)`.
|
||||||
|
*
|
||||||
|
* presentation/app никогда не «открывают» этот тип — они только перепасовывают
|
||||||
|
* объект из [VaultLinkOutcome.Success] в [VaultRegistrar.register].
|
||||||
|
*/
|
||||||
|
interface VaultRegistration
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.github.nullptroma.wallenc.vaultapi
|
||||||
|
|
||||||
|
import com.github.nullptroma.wallenc.domain.interfaces.IVault
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Хелперы фильтрации списка vault'ов по [VaultDescriptor].
|
||||||
|
*
|
||||||
|
* Все реализации vault'а обязаны быть [DescribedVault];
|
||||||
|
* [described] отбрасывает те, что не соответствуют контракту (на случай
|
||||||
|
* вспомогательных тест-двойников и т.п.).
|
||||||
|
*/
|
||||||
|
fun List<IVault>.described(): List<DescribedVault> = filterIsInstance<DescribedVault>()
|
||||||
|
|
||||||
|
val List<DescribedVault>.locals: List<DescribedVault>
|
||||||
|
get() = filter { it.descriptor is VaultDescriptor.LocalDevice }
|
||||||
|
|
||||||
|
val List<DescribedVault>.remotes: List<DescribedVault>
|
||||||
|
get() = filter { it.descriptor is VaultDescriptor.LinkedRemote }
|
||||||
|
|
||||||
|
fun List<DescribedVault>.byBrand(brand: CloudBrand): List<DescribedVault> = filter {
|
||||||
|
(it.descriptor as? VaultDescriptor.LinkedRemote)?.brand == brand
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user