From c81c6ec097f408b0b95a8ba1d994003f2d607f0d Mon Sep 17 00:00:00 2001 From: Krainov Gleb Date: Mon, 12 Oct 2026 08:36:10 +0000 Subject: [PATCH] fix(auto-upload): restart folder runs that stay enqueued on rescan The periodic rescan only cancelled enqueued runs that were periodic, but per-folder runs are one-time work. A run stuck in ENQUEUED stayed "scheduled", so ExistingWorkPolicy.KEEP swallowed every later request for that folder from the content observer, app start and the rescan itself. Cancel every enqueued run except explicit "sync now" runs so the rescan enqueues them again. Signed-off-by: Krainov Gleb Assisted-by: Claude-Code:claude-opus-5-5 --- .../client/jobs/BackgroundJobManagerImpl.kt | 6 +- .../jobs/BackgroundJobManagerImplTest.kt | 68 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 app/src/test/java/com/nextcloud/client/jobs/BackgroundJobManagerImplTest.kt diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index 122b5a8ddd59..e0064c84d399 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -503,13 +503,17 @@ internal class BackgroundJobManagerImpl( override fun isAutoUploadIgnoringPowerSavingScheduled(syncedFolderID: Long): Boolean = workManager.isWorkScheduled(autoUploadIgnorePowerSavingTag(syncedFolderID)) + // An enqueued run that never starts still counts as scheduled and, with ExistingWorkPolicy.KEEP, swallows every + // later request for its folder. Explicit "sync now" runs stay, because restarting them would drop the override. override fun cancelEnqueuedAutoUploads() { workManager.getWorkInfosByTag(formatClassTag(AutoUploadWorker::class)) .get() - .filter { it.state == WorkInfo.State.ENQUEUED && it.periodicityInfo != null } + .filter { it.state == WorkInfo.State.ENQUEUED && !it.isIgnoringPowerSaving() } .forEach { workManager.cancelWorkById(it.id) } } + private fun WorkInfo.isIgnoringPowerSaving(): Boolean = tags.any { it.endsWith("_$TAG_SUFFIX_IGNORE_POWER_SAVING") } + override fun schedulePeriodicAutoUpload() { val request = periodicRequestBuilder( jobClass = AutoUploadRescanWorker::class, diff --git a/app/src/test/java/com/nextcloud/client/jobs/BackgroundJobManagerImplTest.kt b/app/src/test/java/com/nextcloud/client/jobs/BackgroundJobManagerImplTest.kt new file mode 100644 index 000000000000..cc5e003dadbf --- /dev/null +++ b/app/src/test/java/com/nextcloud/client/jobs/BackgroundJobManagerImplTest.kt @@ -0,0 +1,68 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs + +import androidx.work.WorkInfo +import androidx.work.WorkManager +import com.google.common.util.concurrent.Futures +import com.nextcloud.client.core.Clock +import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker +import com.nextcloud.client.preferences.AppPreferences +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify +import java.util.UUID + +class BackgroundJobManagerImplTest { + + private val classTag = BackgroundJobManagerImpl.formatClassTag(AutoUploadWorker::class) + private val workManager: WorkManager = mock() + private val manager = BackgroundJobManagerImpl(workManager, mock(), mock()) + + private fun autoUploadWork(state: WorkInfo.State, vararg extraTags: String): WorkInfo = + WorkInfo(UUID.randomUUID(), state, setOf(classTag, *extraTags)) + + private fun givenAutoUploadWorks(vararg works: WorkInfo) { + workManager.stub { on { getWorkInfosByTag(classTag) } doReturn Futures.immediateFuture(works.toList()) } + } + + @Test + fun `rescan restarts an enqueued folder run that never started`() { + val stuck = autoUploadWork(WorkInfo.State.ENQUEUED, "name:immediate_files_sync_1") + givenAutoUploadWorks(stuck) + + manager.cancelEnqueuedAutoUploads() + + verify(workManager).cancelWorkById(stuck.id) + } + + @Test + fun `rescan leaves running and finished folder runs alone`() { + val running = autoUploadWork(WorkInfo.State.RUNNING) + val succeeded = autoUploadWork(WorkInfo.State.SUCCEEDED) + givenAutoUploadWorks(running, succeeded) + + manager.cancelEnqueuedAutoUploads() + + verify(workManager, never()).cancelWorkById(any()) + } + + @Test + fun `rescan keeps an enqueued sync now run that ignores power saving`() { + val syncNow = autoUploadWork(WorkInfo.State.ENQUEUED, "immediate_files_sync_1_ignore_power_saving") + givenAutoUploadWorks(syncNow) + + manager.cancelEnqueuedAutoUploads() + + verify(workManager, never()).cancelWorkById(any()) + } +}