v1.0.63: live sync progress counters, pause/resume, .gitignore fix
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>
This commit is contained in:
2026-05-27 20:07:25 +00:00
parent 21b7ffc7b3
commit c60eb8d27b
14 changed files with 227 additions and 36 deletions
@@ -3,6 +3,7 @@ package com.syncflow.ui.pairdetail
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.syncflow.data.db.SyncConflictDao
import com.syncflow.data.db.SyncEventDao
@@ -10,7 +11,9 @@ import com.syncflow.data.db.SyncPairDao
import com.syncflow.domain.model.SyncStatus
import com.syncflow.worker.SyncWorker
import dagger.hilt.android.lifecycle.HiltViewModel
import com.syncflow.ui.shared.SyncProgress
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -35,6 +38,19 @@ class PairDetailViewModel @Inject constructor(
val unresolvedConflicts = conflictDao.observeUnresolvedCount(pairId)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0)
val syncProgress = workManager.getWorkInfosByTagFlow("sync_$pairId")
.map { infos ->
infos.firstOrNull { it.state == WorkInfo.State.RUNNING }?.progress?.let { data ->
SyncProgress(
uploaded = data.getInt(SyncWorker.KEY_PROGRESS_UPLOADED, 0),
downloaded = data.getInt(SyncWorker.KEY_PROGRESS_DOWNLOADED, 0),
deleted = data.getInt(SyncWorker.KEY_PROGRESS_DELETED, 0),
bytesTransferred = data.getLong(SyncWorker.KEY_PROGRESS_BYTES, 0L),
).takeIf { it.uploaded > 0 || it.downloaded > 0 || it.deleted > 0 }
}
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
fun syncNow() {
val p = pair.value ?: return
workManager.enqueue(SyncWorker.buildOneTimeRequest(p.id, wifiOnly = false, chargingOnly = false))