feat(sync): добавил механизм синхронизации хранилищ и управление группами
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -63,8 +63,11 @@ dependencies {
|
||||
// Hilt
|
||||
implementation(libs.dagger.hilt)
|
||||
ksp(libs.dagger.hilt.compiler)
|
||||
implementation(libs.androidx.hilt.work)
|
||||
ksp(libs.androidx.hilt.compiler)
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
package com.github.nullptroma.wallenc.app
|
||||
|
||||
import android.app.Application
|
||||
import androidx.work.Configuration
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import com.github.nullptroma.wallenc.app.sync.StorageSyncBootstrap
|
||||
import com.github.nullptroma.wallenc.app.tasks.TaskPipelineForegroundBootstrap
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class WallencApplication : Application() {
|
||||
class WallencApplication : Application(), Configuration.Provider {
|
||||
|
||||
@Inject
|
||||
lateinit var taskPipelineForegroundBootstrap: TaskPipelineForegroundBootstrap
|
||||
|
||||
@Inject
|
||||
lateinit var storageSyncBootstrap: StorageSyncBootstrap
|
||||
|
||||
@Inject
|
||||
lateinit var workerFactory: HiltWorkerFactory
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
taskPipelineForegroundBootstrap.start()
|
||||
storageSyncBootstrap.start()
|
||||
}
|
||||
|
||||
override val workManagerConfiguration: Configuration
|
||||
get() = Configuration.Builder()
|
||||
.setWorkerFactory(workerFactory)
|
||||
.build()
|
||||
}
|
||||
@@ -15,10 +15,12 @@ import com.github.nullptroma.wallenc.infrastructure.network.yandexuserinfo.Yande
|
||||
import com.github.nullptroma.wallenc.infrastructure.network.yandexuserinfo.repository.YandexUserInfoRepository
|
||||
import com.github.nullptroma.wallenc.infrastructure.ports.StorageKeyMapStore
|
||||
import com.github.nullptroma.wallenc.infrastructure.ports.YandexAccountStore
|
||||
import com.github.nullptroma.wallenc.app.sync.StorageSyncGroupStore
|
||||
import com.github.nullptroma.wallenc.task.runtime.TaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.infrastructure.vaults.VaultsManager
|
||||
import com.github.nullptroma.wallenc.infrastructure.vaults.local.LocalVault
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncGroupStore
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IVault
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
@@ -133,6 +135,12 @@ class SingletonModule {
|
||||
return StorageMetaInfoRepository(dao, ioDispatcher)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideStorageSyncGroupStore(
|
||||
@ApplicationContext context: Context,
|
||||
): IStorageSyncGroupStore = StorageSyncGroupStore(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideYandexAccountRepository(
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
package com.github.nullptroma.wallenc.app.di.modules.domain
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncEngine
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncGroupStore
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
|
||||
import com.github.nullptroma.wallenc.usecases.GetOpenedStoragesUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.ManageStoragesEncryptionUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.ManageVaultUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.RemoveStorageUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.RenameStorageUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.RunStorageSyncUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.ManageStorageSyncGroupsUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.StorageSyncEngine
|
||||
import com.github.nullptroma.wallenc.usecases.StorageFileManagementUseCase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -58,4 +64,27 @@ class UseCasesModule {
|
||||
): RemoveStorageUseCase {
|
||||
return RemoveStorageUseCase(vaultsManager, unlockManager, manageStoragesEncryptionUseCase)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideStorageSyncEngine(
|
||||
vaultsManager: IVaultsManager,
|
||||
groupStore: IStorageSyncGroupStore,
|
||||
): IStorageSyncEngine = StorageSyncEngine(
|
||||
vaultsManager = vaultsManager,
|
||||
groupStore = groupStore,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideManageStorageSyncGroupsUseCase(
|
||||
groupStore: IStorageSyncGroupStore,
|
||||
): ManageStorageSyncGroupsUseCase = ManageStorageSyncGroupsUseCase(groupStore)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRunStorageSyncUseCase(
|
||||
orchestrator: ITaskOrchestrator,
|
||||
syncEngine: IStorageSyncEngine,
|
||||
): RunStorageSyncUseCase = RunStorageSyncUseCase(orchestrator, syncEngine)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.github.nullptroma.wallenc.app.sync
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
|
||||
import com.github.nullptroma.wallenc.usecases.RunStorageSyncUseCase
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
@OptIn(FlowPreview::class)
|
||||
class StorageSyncBootstrap @Inject constructor(
|
||||
private val scheduler: StorageSyncScheduler,
|
||||
private val vaultsManager: IVaultsManager,
|
||||
private val syncRunner: RunStorageSyncUseCase,
|
||||
) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
fun start() {
|
||||
scheduler.ensureScheduled()
|
||||
scope.launch {
|
||||
vaultsManager.allStorages.collectLatest { storages ->
|
||||
if (storages.isEmpty()) {
|
||||
return@collectLatest
|
||||
}
|
||||
val triggers = storages.flatMap { storage ->
|
||||
listOf(
|
||||
storage.accessor.filesUpdates.map { Unit },
|
||||
storage.accessor.dirsUpdates.map { Unit },
|
||||
)
|
||||
}
|
||||
merge(*triggers.toTypedArray())
|
||||
.debounce(DEBOUNCE_AFTER_CHANGE_MS)
|
||||
.collect {
|
||||
syncRunner.enqueue("debounce")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val DEBOUNCE_AFTER_CHANGE_MS = 60_000L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.github.nullptroma.wallenc.app.sync
|
||||
|
||||
import android.content.Context
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncGroupStore
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.StorageSyncGroup
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import androidx.core.content.edit
|
||||
|
||||
@Singleton
|
||||
class StorageSyncGroupStore @Inject constructor(
|
||||
@param:ApplicationContext private val app: Context,
|
||||
) : IStorageSyncGroupStore {
|
||||
private val prefs by lazy {
|
||||
app.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
override suspend fun getGroups(): List<StorageSyncGroup> {
|
||||
val raw = prefs.getString(KEY_GROUPS, null).orEmpty()
|
||||
return parse(raw)
|
||||
}
|
||||
|
||||
override suspend fun putGroup(group: StorageSyncGroup) {
|
||||
val current = getGroups().toMutableList()
|
||||
val idx = current.indexOfFirst { it.id == group.id }
|
||||
if (idx >= 0) {
|
||||
current[idx] = group
|
||||
} else {
|
||||
current.add(group)
|
||||
}
|
||||
persist(current)
|
||||
}
|
||||
|
||||
override suspend fun removeGroup(groupId: String) {
|
||||
val current = getGroups().filterNot { it.id == groupId }
|
||||
persist(current)
|
||||
}
|
||||
|
||||
private fun parse(raw: String): List<StorageSyncGroup> {
|
||||
if (raw.isBlank()) {
|
||||
return emptyList()
|
||||
}
|
||||
return raw.lineSequence()
|
||||
.mapNotNull { line ->
|
||||
val trimmed = line.trim()
|
||||
if (trimmed.isBlank()) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
val split = trimmed.split("=", limit = 2)
|
||||
if (split.size != 2) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
val id = split[0].trim()
|
||||
if (id.isBlank()) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
val uuids = split[1]
|
||||
.split(",")
|
||||
.mapNotNull { token ->
|
||||
val value = token.trim()
|
||||
if (value.isBlank()) {
|
||||
null
|
||||
} else {
|
||||
runCatching { UUID.fromString(value) }.getOrNull()
|
||||
}
|
||||
}
|
||||
.toSet()
|
||||
StorageSyncGroup(id = id, storageUuids = uuids)
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun persist(groups: List<StorageSyncGroup>) {
|
||||
val raw = groups.joinToString(separator = "\n") { group ->
|
||||
val uuids = group.storageUuids.joinToString(separator = ",") { it.toString() }
|
||||
"${group.id}=$uuids"
|
||||
}
|
||||
prefs.edit { putString(KEY_GROUPS, raw) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val PREFS_NAME = "wallenc_storage_sync"
|
||||
private const val KEY_GROUPS = "groups"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.github.nullptroma.wallenc.app.sync
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class StorageSyncScheduler @Inject constructor(
|
||||
@param:ApplicationContext private val app: Context,
|
||||
) {
|
||||
fun ensureScheduled() {
|
||||
val request = PeriodicWorkRequestBuilder<StorageSyncWorker>(
|
||||
repeatInterval = 30,
|
||||
repeatIntervalTimeUnit = TimeUnit.MINUTES,
|
||||
)
|
||||
.setConstraints(
|
||||
Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(app).enqueueUniquePeriodicWork(
|
||||
StorageSyncWorker.UNIQUE_WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
request,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.github.nullptroma.wallenc.app.sync
|
||||
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.github.nullptroma.wallenc.usecases.RunStorageSyncUseCase
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
@HiltWorker
|
||||
class StorageSyncWorker @AssistedInject constructor(
|
||||
@Assisted appContext: Context,
|
||||
@Assisted params: WorkerParameters,
|
||||
private val syncRunner: RunStorageSyncUseCase,
|
||||
) : CoroutineWorker(appContext, params) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
return runCatching {
|
||||
syncRunner.runBlocking()
|
||||
Result.success()
|
||||
}.getOrElse {
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val UNIQUE_WORK_NAME = "wallenc-storage-sync-periodic"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user