diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index 7f4a0a3184cc..ebcc2a1ebe32 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -147,7 +147,7 @@ interface BackgroundJobManager { fun isAutoUploadScheduled(syncedFolderID: Long): Boolean fun isStartFileUploadJobScheduled(accountName: String): Boolean fun isAutoUploadIgnoringPowerSavingScheduled(syncedFolderID: Long): Boolean - fun schedulePeriodicAutoUpload() + fun schedulePeriodicAutoUpload(resetSchedule: Boolean = false) fun cancelEnqueuedAutoUploads() fun cancelFilesDownloadJob(accountName: String, fileId: Long) 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 54f3852d8763..c2f16911b610 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -512,14 +512,19 @@ internal class BackgroundJobManagerImpl( .forEach { workManager.cancelWorkById(it.id) } } - override fun schedulePeriodicAutoUpload() { + override fun schedulePeriodicAutoUpload(resetSchedule: Boolean) { val request = periodicRequestBuilder( jobClass = AutoUploadRescanWorker::class, jobName = JOB_PERIODIC_FILES_SYNC, constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() ).build() - workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_FILES_SYNC, ExistingPeriodicWorkPolicy.KEEP, request) + val policy = if (resetSchedule) { + ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE + } else { + ExistingPeriodicWorkPolicy.KEEP + } + workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_FILES_SYNC, policy, request) } override fun startAutoUpload(syncedFolder: SyncedFolder, overridePowerSaving: Boolean) { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/uploadList/UploadListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/uploadList/UploadListFragment.kt index 74306573276b..d7156caa5c51 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/uploadList/UploadListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/uploadList/UploadListFragment.kt @@ -257,7 +257,8 @@ class UploadListFragment : activity, preferences, userAccountManager, - uploadsStorageManager + uploadsStorageManager, + backgroundJobManager ) { uploadListAdapter.notifyDataSetChanged() } activity.addMenuProvider(menuProvider, viewLifecycleOwner, Lifecycle.State.RESUMED) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/uploadList/UploadListMenuProvider.kt b/app/src/main/java/com/owncloud/android/ui/fragment/uploadList/UploadListMenuProvider.kt index da637f6bcf75..4dda9dbecbd6 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/uploadList/UploadListMenuProvider.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/uploadList/UploadListMenuProvider.kt @@ -12,6 +12,7 @@ import android.view.MenuInflater import android.view.MenuItem import androidx.core.view.MenuProvider import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.upload.AlbumFileUploadWorker import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker @@ -24,6 +25,7 @@ class UploadListMenuProvider( private val preferences: AppPreferences, private val userAccountManager: UserAccountManager, private val uploadsStorageManager: UploadsStorageManager, + private val backgroundJobManager: BackgroundJobManager, private val onGlobalPauseToggled: () -> Unit ) : MenuProvider { @@ -68,6 +70,9 @@ class UploadListMenuProvider( val ids = uploadsStorageManager.getCurrentUploadIds(user.accountName) uploadHelper.cancelAndRestartUploadJob(user, ids) } + if (!paused) { + backgroundJobManager.schedulePeriodicAutoUpload(resetSchedule = true) + } onGlobalPauseToggled() } } diff --git a/app/src/test/java/com/nextcloud/client/jobs/AutoUploadScheduleTest.kt b/app/src/test/java/com/nextcloud/client/jobs/AutoUploadScheduleTest.kt new file mode 100644 index 000000000000..94d1ab4290f7 --- /dev/null +++ b/app/src/test/java/com/nextcloud/client/jobs/AutoUploadScheduleTest.kt @@ -0,0 +1,74 @@ +/* + * 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.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import com.nextcloud.client.jobs.autoUpload.AutoUploadRescanWorker +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import java.util.concurrent.TimeUnit + +class AutoUploadScheduleTest { + + private val workManager: WorkManager = mock() + private val manager: BackgroundJobManager = BackgroundJobManagerImpl(workManager, mock(), mock()) + + @Test + fun `startup preserves the existing periodic scan`() { + manager.schedulePeriodicAutoUpload() + + verify(workManager).enqueueUniquePeriodicWork( + eq(BackgroundJobManagerImpl.JOB_PERIODIC_FILES_SYNC), + eq(ExistingPeriodicWorkPolicy.KEEP), + any() + ) + verifyNoMoreInteractions(workManager) + } + + @Test + fun `resume replaces only the periodic coordinator with an immediately eligible scan`() { + manager.schedulePeriodicAutoUpload(resetSchedule = true) + + val requests = argumentCaptor() + verify(workManager).enqueueUniquePeriodicWork( + eq(BackgroundJobManagerImpl.JOB_PERIODIC_FILES_SYNC), + eq(ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE), + requests.capture() + ) + verifyNoMoreInteractions(workManager) + + val request = requests.firstValue + val spec = request.workSpec + assertEquals(AutoUploadRescanWorker::class.java.name, spec.workerClassName) + assertEquals(NetworkType.CONNECTED, spec.constraints.requiredNetworkType) + assertEquals(TimeUnit.MINUTES.toMillis(15), spec.intervalDuration) + assertEquals(spec.intervalDuration, spec.flexDuration) + assertEquals(0L, spec.initialDelay) + spec.lastEnqueueTime = ENQUEUE_TIME + assertEquals(ENQUEUE_TIME, spec.calculateNextRunTime()) + assertTrue(request.tags.contains(BackgroundJobManagerImpl.TAG_ALL)) + assertTrue( + request.tags.contains( + BackgroundJobManagerImpl.formatNameTag(BackgroundJobManagerImpl.JOB_PERIODIC_FILES_SYNC) + ) + ) + } + + companion object { + private const val ENQUEUE_TIME = 1_000_000L + } +} diff --git a/app/src/test/java/com/owncloud/android/ui/fragment/uploadList/UploadListMenuProviderTest.kt b/app/src/test/java/com/owncloud/android/ui/fragment/uploadList/UploadListMenuProviderTest.kt new file mode 100644 index 000000000000..6e69f1df8946 --- /dev/null +++ b/app/src/test/java/com/owncloud/android/ui/fragment/uploadList/UploadListMenuProviderTest.kt @@ -0,0 +1,100 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.fragment.uploadList + +import android.content.Context +import android.view.MenuItem +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.jobs.BackgroundJobManager +import com.nextcloud.client.jobs.upload.AlbumFileUploadWorker +import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.client.preferences.AppPreferences +import com.owncloud.android.R +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import io.mockk.verify +import io.mockk.verifyOrder +import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class UploadListMenuProviderTest { + + private val preferences = mockk(relaxed = true) + private val manager = mockk(relaxed = true) + private val accountManager = mockk() + private val onPauseToggled = mockk<() -> Unit>(relaxed = true) + private val menuItem = mockk(relaxed = true) + private var paused = false + private val provider = UploadListMenuProvider( + mockk(relaxed = true), + preferences, + accountManager, + mockk(), + manager, + onPauseToggled + ) + + @Before + fun setUp() { + mockkObject(FileUploadHelper, FileUploadWorker, AlbumFileUploadWorker) + every { FileUploadHelper.instance() } returns mockk(relaxed = true) + every { FileUploadWorker.pauseActiveUploads() } just Runs + every { AlbumFileUploadWorker.pauseActiveUploads() } just Runs + every { accountManager.allUsers } returns emptyList() + every { preferences.isGlobalUploadPaused() } answers { paused } + every { preferences.setGlobalUploadPaused(any()) } answers { paused = firstArg() } + every { menuItem.itemId } returns R.id.action_toggle_global_pause + } + + @After + fun tearDown() { + unmockkObject(FileUploadHelper, FileUploadWorker, AlbumFileUploadWorker) + } + + @Test + fun `resume clears the pause before resetting the automatic scan`() { + paused = true + + assertTrue(provider.onMenuItemSelected(menuItem)) + + assertFalse(paused) + verifyOrder { + preferences.setGlobalUploadPaused(false) + manager.schedulePeriodicAutoUpload(resetSchedule = true) + onPauseToggled() + } + verify(exactly = 1) { manager.schedulePeriodicAutoUpload(true) } + } + + @Test + fun `pause stops active transfers without restarting automatic scans`() { + assertTrue(provider.onMenuItemSelected(menuItem)) + + assertTrue(paused) + verify { FileUploadWorker.pauseActiveUploads() } + verify { AlbumFileUploadWorker.pauseActiveUploads() } + verify(exactly = 0) { manager.schedulePeriodicAutoUpload(any()) } + } + + @Test + fun `unrelated menu actions leave the scan schedule alone`() { + every { menuItem.itemId } returns R.id.action_search + + assertFalse(provider.onMenuItemSelected(menuItem)) + + verify(exactly = 0) { preferences.setGlobalUploadPaused(any()) } + verify(exactly = 0) { manager.schedulePeriodicAutoUpload(any()) } + } +} diff --git a/app/src/test/java/com/owncloud/android/utils/FilesSyncHelperTest.kt b/app/src/test/java/com/owncloud/android/utils/FilesSyncHelperTest.kt index e25ab1b23d0d..7a2abbd8a2f1 100644 --- a/app/src/test/java/com/owncloud/android/utils/FilesSyncHelperTest.kt +++ b/app/src/test/java/com/owncloud/android/utils/FilesSyncHelperTest.kt @@ -83,4 +83,22 @@ class FilesSyncHelperTest { assertEquals(1, startedFolderCount) verify(backgroundJobManager).startAutoUpload(enabled, false) } + + @Test + fun `rescan preserves scheduled folder work and starts only idle enabled folders`() { + val scheduled = syncedFolder(id = 1, enabled = true) + val idle = syncedFolder(id = 2, enabled = true) + val disabled = syncedFolder(id = 3, enabled = false) + whenever(backgroundJobManager.isAutoUploadScheduled(scheduled.id)).thenReturn(true) + + val count = FilesSyncHelper.startAutoUploadForEnabledSyncedFoldersIfNotActive( + provider(scheduled, idle, disabled), + backgroundJobManager + ) + + assertEquals(1, count) + verify(backgroundJobManager, never()).startAutoUpload(scheduled, false) + verify(backgroundJobManager, never()).startAutoUpload(disabled, false) + verify(backgroundJobManager).startAutoUpload(idle, false) + } }