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
@@ -4,16 +4,20 @@ 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
@@ -28,6 +32,23 @@ class HomeViewModel @Inject constructor(
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)