diff --git a/app/src/main/kotlin/com/syncflow/worker/FileWatchService.kt b/app/src/main/kotlin/com/syncflow/worker/FileWatchService.kt index 5ee01fc..c3e5d93 100644 --- a/app/src/main/kotlin/com/syncflow/worker/FileWatchService.kt +++ b/app/src/main/kotlin/com/syncflow/worker/FileWatchService.kt @@ -44,8 +44,10 @@ class FileWatchService : Service() { private val fileObservers = mutableMapOf>() private val contentObservers = mutableMapOf() private val debounceJobs = mutableMapOf() - // After a watcher-triggered sync completes, suppress FileObserver events for this long - // to stop the feedback loop: sync writes files → FileObserver fires → another sync → repeat. + // Persistent monitors that watch WorkManager for ANY sync (manual, catchup, onchange) + // so the cooldown is set regardless of who triggered the sync. + private val syncMonitorJobs = mutableMapOf() + // After a sync completes, suppress FileObserver events for this long. private val syncCooldownUntil = mutableMapOf() companion object { @@ -154,9 +156,36 @@ class FileWatchService : Service() { syncCooldownUntil[pairId] = System.currentTimeMillis() + 15_000 watchDirRecursive(dir, pairId, wifiOnly, chargingOnly) Timber.d("FileWatchService: watching pair $pairId at $path (${fileObservers[pairId]?.size} dirs)") + startSyncMonitor(pairId) scope.launch { catchupScan(pairId, dir, wifiOnly, chargingOnly) } } + // Watches WorkManager for ANY sync tagged sync_$pairId (manual, catchup, onchange). + // Sets cooldown while running and for 60s after, so FileObserver events from our + // own file writes never trigger a re-sync regardless of what started the sync. + private fun startSyncMonitor(pairId: Long) { + syncMonitorJobs[pairId]?.cancel() + syncMonitorJobs[pairId] = scope.launch { + var wasSyncing = false + WorkManager.getInstance(applicationContext) + .getWorkInfosByTagFlow("sync_$pairId") + .collect { infos -> + val isSyncing = infos.any { + it.state == WorkInfo.State.RUNNING || it.state == WorkInfo.State.ENQUEUED + } + if (isSyncing) { + Timber.d("FileWatchService: sync active for pair $pairId — cooldown extended") + syncCooldownUntil[pairId] = System.currentTimeMillis() + 120_000 + wasSyncing = true + } else if (wasSyncing) { + Timber.d("FileWatchService: sync finished for pair $pairId — 60s settle cooldown") + syncCooldownUntil[pairId] = System.currentTimeMillis() + 60_000 + wasSyncing = false + } + } + } + } + private fun watchDirRecursive(dir: File, pairId: Long, wifiOnly: Boolean, chargingOnly: Boolean) { if (!dir.isDirectory) return val mask = FileObserver.CREATE or FileObserver.DELETE or FileObserver.MODIFY or @@ -297,6 +326,8 @@ class FileWatchService : Service() { contentObservers.clear() debounceJobs.values.forEach { it.cancel() } debounceJobs.clear() + syncMonitorJobs.values.forEach { it.cancel() } + syncMonitorJobs.clear() syncCooldownUntil.clear() } diff --git a/version.properties b/version.properties index 581ccc6..b2a8657 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=1.0.31 -VERSION_CODE=32 +VERSION_NAME=1.0.32 +VERSION_CODE=33