c60eb8d27b
Build & Release APK / build (push) Has been cancelled
- 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>
90 lines
3.9 KiB
Kotlin
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|