Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ class FileSystemRepository(

suspend fun hasPendingFiles(syncedFolder: SyncedFolder): Boolean = dao.hasPendingFiles(syncedFolder.id.toString())

fun hasPendingFile(localPath: String, syncedFolder: SyncedFolder): Boolean {
val entity = dao.getFileByPathAndFolder(localPath, syncedFolder.id.toString()) ?: return false
return entity.fileSentForUpload == 0 && entity.fileIsFolder == 0
}

suspend fun deleteByLocalPathAndId(path: String, id: Int) {
dao.deleteByLocalPathAndId(path, id)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,15 +305,6 @@ class FileUploadWorker(

delay(retryPolicy.getDelay().milliseconds)

if (!skipAutoUploadCheck && isBelongToAnySyncedFolder(upload, syncFolderHelper, syncedFolders)) {
Log_OC.d(TAG, "skipping upload, will be handled by AutoUploadWorker: ${upload.localPath}")
uploadsStorageManager.uploadDao.deleteByRemotePathAndAccountName(
remotePath = upload.remotePath,
accountName = accountName
)
continue
}

if (preferences.isGlobalUploadPaused) {
Log_OC.d(TAG, "Upload is paused, skip uploading files!")
notificationManager.notifyPaused(
Expand All @@ -322,6 +313,11 @@ class FileUploadWorker(
return@withContext Result.success()
}

if (!skipAutoUploadCheck && scheduleAutoUploadRecovery(upload, syncFolderHelper, syncedFolders)) {
Log_OC.d(TAG, "scheduled auto upload recovery, preserving upload record: ${upload.localPath}")
continue
}

if (canExitEarly()) {
notificationManager.showConnectionErrorNotification()
return@withContext Result.retry()
Expand Down Expand Up @@ -385,18 +381,21 @@ class FileUploadWorker(
else -> false
}

suspend fun isBelongToAnySyncedFolder(
private fun scheduleAutoUploadRecovery(
upload: OCUpload,
syncFolderHelper: SyncFolderHelper,
syncedFolders: List<SyncedFolder>
syncedFolders: Iterable<SyncedFolder>
): Boolean {
if (!filesystemRepository.isBelongToAnyAutoFolder(upload.localPath)) return false

return syncedFolders.any { folder ->
val file = upload.localPath.toFile() ?: return false
val expectedRemotePath = syncFolderHelper.getAutoUploadRemotePath(folder, file)
expectedRemotePath == upload.remotePath
}
val file = upload.localPath.toFile() ?: return false
val folder = syncedFolders.firstOrNull {
it.isEnabled && it.account == upload.accountName &&
filesystemRepository.hasPendingFile(upload.localPath, it) &&
syncFolderHelper.getAutoUploadRemotePath(it, file) == upload.remotePath
} ?: return false

// The auto upload worker reuses this record, including any E2EE recovery token.
backgroundJobManager.startAutoUpload(folder)
return true
}

private fun sendUploadFinishEvent(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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 android.net.NetworkRequest
import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker
import com.owncloud.android.datamodel.SyncedFolder
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mockito.Answers.RETURNS_SELF
import org.mockito.Mockito.mockConstruction
import org.mockito.Mockito.withSettings
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import java.util.concurrent.TimeUnit

class AutoUploadRecoveryScheduleTest {
private val workManager: WorkManager = mock()
private val manager: BackgroundJobManager = BackgroundJobManagerImpl(workManager, mock(), mock())

@Test
fun `recovery preserves existing folder work with network charging and retry constraints`() {
val folder: SyncedFolder = mock {
on { id } doReturn 3L
on { isChargingOnly } doReturn true
}
mockConstruction(NetworkRequest.Builder::class.java, withSettings().defaultAnswer(RETURNS_SELF)) { builder, _ ->
whenever(builder.build()).thenReturn(mock())
}.use {
manager.startAutoUpload(folder)
manager.startAutoUpload(folder)
}

val requests = argumentCaptor<OneTimeWorkRequest>()
verify(workManager, times(2)).enqueueUniqueWork(
eq(BackgroundJobManagerImpl.JOB_IMMEDIATE_FILES_SYNC + "_3"),
eq(ExistingWorkPolicy.KEEP),
requests.capture()
)
verifyNoMoreInteractions(workManager)

val spec = requests.firstValue.workSpec
assertEquals(AutoUploadWorker::class.java.name, spec.workerClassName)
assertEquals(NetworkType.CONNECTED, spec.constraints.requiredNetworkType)
assertTrue(spec.constraints.requiresCharging())
assertEquals(BackoffPolicy.LINEAR, spec.backoffPolicy)
assertEquals(
TimeUnit.SECONDS.toMillis(BackgroundJobManagerImpl.DEFAULT_BACKOFF_CRITERIA_DELAY_SEC),
spec.backoffDelayDuration
)
assertEquals(3L, spec.input.getLong(AutoUploadWorker.SYNCED_FOLDER_ID, -1))
assertFalse(spec.input.getBoolean(AutoUploadWorker.OVERRIDE_POWER_SAVING, true))
}
}
Loading