Files
SyncFlow/app/src/main/kotlin/com/syncflow/ui/home/HomeViewModel.kt
T
amir c60eb8d27b
Build & Release APK / build (push) Has been cancelled
v1.0.63: live sync progress counters, pause/resume, .gitignore fix
- SyncEngine: accepts onProgress callback — emits uploaded/downloaded/
  deleted/bytes counts atomically as each file completes
- SyncWorker: streams progress to WorkManager data so the UI can poll
  it live; reports per-run counters in the completion notification;
  adds pause/resume support
- HomeViewModel/PairDetailViewModel: subscribe to live WorkManager
  progress and surface it via SyncProgress state
- SyncPairEntity/SyncPairDao/SyncDatabase: persist last-run counters
  (uploaded, downloaded, deleted, bytesTransferred) in the DB with a
  Room migration (v3→v4)
- AppModule: provides WorkManager as an injectable singleton
- .gitignore: add .kotlin/ to exclude compiler session files

Security: no new issues — all logging via Timber (debug-only), DB
queries use Room parameterized API, file sharing via FileProvider.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:07:25 +00:00

90 lines
3.9 KiB
Kotlin

package com.syncflow.ui.home
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.syncflow.data.db.SyncPairDao
import com.syncflow.data.db.entities.SyncPairEntity
import com.syncflow.domain.model.ScheduleType
import com.syncflow.domain.model.SyncStatus
import com.syncflow.ui.shared.SyncProgress
import com.syncflow.worker.FileWatchService
import com.syncflow.worker.SyncWorker
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
private val syncPairDao: SyncPairDao,
private val workManager: WorkManager,
@ApplicationContext private val context: Context,
) : ViewModel() {
val syncPairs = syncPairDao.observeAll()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
val syncProgressMap: kotlinx.coroutines.flow.StateFlow<Map<Long, SyncProgress>> =
workManager.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.RUNNING))
.map { infos ->
infos
.mapNotNull { info ->
val tag = info.tags.firstOrNull { it.startsWith("sync_") } ?: return@mapNotNull null
val pairId = tag.removePrefix("sync_").toLongOrNull() ?: return@mapNotNull null
val up = info.progress.getInt(SyncWorker.KEY_PROGRESS_UPLOADED, 0)
val down = info.progress.getInt(SyncWorker.KEY_PROGRESS_DOWNLOADED, 0)
val del = info.progress.getInt(SyncWorker.KEY_PROGRESS_DELETED, 0)
val bytes = info.progress.getLong(SyncWorker.KEY_PROGRESS_BYTES, 0L)
if (up > 0 || down > 0 || del > 0) pairId to SyncProgress(up, down, del, bytes) else null
}
.toMap()
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
fun triggerSync(pair: SyncPairEntity) {
val req = SyncWorker.buildOneTimeRequest(pair.id, wifiOnly = false, chargingOnly = false)
workManager.enqueue(req)
}
fun pauseSync(pair: SyncPairEntity) {
workManager.cancelAllWorkByTag("sync_${pair.id}")
viewModelScope.launch { syncPairDao.updateStatus(pair.id, SyncStatus.PAUSED) }
}
fun toggleEnabled(pair: SyncPairEntity) {
viewModelScope.launch {
val nowEnabled = !pair.isEnabled
syncPairDao.update(pair.copy(isEnabled = nowEnabled))
if (nowEnabled) {
when (pair.scheduleType) {
ScheduleType.ON_CHANGE -> FileWatchService.start(context)
ScheduleType.MANUAL -> { /* nothing */ }
else -> {
val req = SyncWorker.buildPeriodicRequest(
pair.id,
pair.scheduleIntervalMinutes.toLong().coerceAtLeast(15),
pair.wifiOnly,
pair.chargingOnly,
)
workManager.enqueueUniquePeriodicWork("periodic_${pair.id}", ExistingPeriodicWorkPolicy.UPDATE, req)
}
}
} else {
workManager.cancelAllWorkByTag("sync_${pair.id}")
// Refresh watcher (it will stop itself if no ON_CHANGE pairs remain)
if (pair.scheduleType == ScheduleType.ON_CHANGE) {
FileWatchService.start(context)
}
}
}
}
}