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 @@ -25,9 +25,9 @@ import com.nextcloud.client.integrations.deck.DeckApi
import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker
import com.nextcloud.client.jobs.autoUpload.FileSystemRepository
import com.nextcloud.client.jobs.download.FileDownloadWorker
import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker
import com.nextcloud.client.jobs.metadata.MetadataWorker
import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker
import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker
import com.nextcloud.client.jobs.upload.FileUploadWorker
import com.nextcloud.client.logger.Logger
import com.nextcloud.client.network.ConnectivityService
Expand Down Expand Up @@ -180,7 +180,8 @@ class BackgroundJobFactory @Inject constructor(
syncedFolderProvider = syncedFolderProvider,
backgroundJobManager = backgroundJobManager.get(),
repository = FileSystemRepository(dao = database.fileSystemDao(), context),
viewThemeUtils = viewThemeUtils.get()
viewThemeUtils = viewThemeUtils.get(),
localBroadcastManager = localBroadcastManager.get()
)

private fun createOfflineSyncWork(context: Context, params: WorkerParameters): OfflineSyncWork = OfflineSyncWork(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import android.app.Notification
import android.content.Context
import android.content.res.Resources
import androidx.exifinterface.media.ExifInterface
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
Expand All @@ -21,6 +22,7 @@ import com.nextcloud.client.database.entity.toOCUpload
import com.nextcloud.client.database.entity.toUploadEntity
import com.nextcloud.client.device.PowerManagementService
import com.nextcloud.client.jobs.BackgroundJobManager
import com.nextcloud.client.jobs.upload.FileUploadBroadcastManager
import com.nextcloud.client.jobs.upload.FileUploadWorker
import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager
import com.nextcloud.client.network.ConnectivityService
Expand All @@ -37,6 +39,7 @@ import com.owncloud.android.db.OCUpload
import com.owncloud.android.db.UploadResult
import com.owncloud.android.lib.common.OwnCloudAccount
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.operations.UploadFileOperation
import com.owncloud.android.ui.activity.SettingsActivity
Expand All @@ -62,7 +65,8 @@ class AutoUploadWorker(
private val syncedFolderProvider: SyncedFolderProvider,
private val backgroundJobManager: BackgroundJobManager,
private val repository: FileSystemRepository,
val viewThemeUtils: ViewThemeUtils
val viewThemeUtils: ViewThemeUtils,
localBroadcastManager: LocalBroadcastManager
) : CoroutineWorker(context, params) {

companion object {
Expand All @@ -74,6 +78,7 @@ class AutoUploadWorker(
}

private val helper = AutoUploadHelper()
private val fileUploadBroadcastManager = FileUploadBroadcastManager(localBroadcastManager)
private lateinit var syncedFolder: SyncedFolder
private val notificationManager = AutoUploadNotificationManager(context, viewThemeUtils, NOTIFICATION_ID)

Expand Down Expand Up @@ -281,6 +286,7 @@ class AutoUploadWorker(
updateNotification()

var lastId = 0

while (true) {
val filePathsWithIds = repository.getFilePathsWithIds(syncedFolder, lastId)

Expand All @@ -290,7 +296,7 @@ class AutoUploadWorker(
}
Log_OC.d(TAG, "Processing batch: lastId=$lastId, count=${filePathsWithIds.size}")

filePathsWithIds.forEach { (path, id) ->
filePathsWithIds.forEachIndexed { batchIndex, (path, id) ->
val file = File(path)
val localPath = file.absolutePath
val remotePath = getRemotePath(
Expand Down Expand Up @@ -325,10 +331,12 @@ class AutoUploadWorker(
uploadEntity = uploadEntity.copy(id = generatedId.toInt())
upload.uploadId = generatedId

fileUploadBroadcastManager.sendAdded(context)
val operation = createUploadFileOperation(upload, user)
Log_OC.d(TAG, "🕒 uploading: $localPath, id: $generatedId")

val result = operation.execute(client)
fileUploadBroadcastManager.sendStarted(operation, context)
uploadsStorageManager.updateStatus(uploadEntity, result.isSuccess)

UploadErrorNotificationManager.handleResult(
Expand All @@ -347,6 +355,11 @@ class AutoUploadWorker(
"❌ upload failed $localPath (${upload.accountName}): ${result.logMessage}"
)
}

val isLastInBatch = (batchIndex == filePathsWithIds.size - 1)
if (isLastInBatch) {
sendUploadFinishEvent(operation, result)
}
} catch (e: Exception) {
uploadsStorageManager.updateStatus(
uploadEntity,
Expand Down Expand Up @@ -515,4 +528,13 @@ class AutoUploadWorker(
}
return lastModificationTime
}

private fun sendUploadFinishEvent(operation: UploadFileOperation, result: RemoteOperationResult<*>) {
fileUploadBroadcastManager.sendFinished(
operation,
result,
operation.oldFile?.storagePath,
context
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.jobs.upload

import android.content.Context
import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.operations.UploadFileOperation

/**
* Manages local broadcasts related to file upload lifecycle events.
*
* This class is responsible for notifying interested components about
* upload queue changes and upload state transitions (added, started, finished).
*
* All broadcasts are sent via [LocalBroadcastManager].
*/
class FileUploadBroadcastManager(private val broadcastManager: LocalBroadcastManager) {

companion object {
private const val TAG = "📣" + "FileUploadBroadcastManager"

const val UPLOAD_ADDED = "UPLOAD_ADDED"
const val UPLOAD_STARTED = "UPLOAD_STARTED"
const val UPLOAD_FINISHED = "UPLOAD_FINISHED"
}

/**
* Sends a broadcast indicating that an upload added into database.
*
* ### Triggered when
* - [UploadFileOperation] added
*
* ### Observed by
* - [com.owncloud.android.ui.activity.UploadListActivity.UploadFinishReceiver]
*
*/
fun sendAdded(context: Context) {
Log_OC.d(TAG, "upload added broadcast sent")
val intent = Intent(UPLOAD_ADDED).apply {
setPackage(context.packageName)
}
broadcastManager.sendBroadcast(intent)
}

/**
* Sends a broadcast indicating that an upload started
*
* ### Triggered when
* - [UploadFileOperation] started
*
* ### Observed by
* - [com.owncloud.android.ui.activity.UploadListActivity.UploadFinishReceiver]
*
*/
fun sendStarted(upload: UploadFileOperation, context: Context) {
Log_OC.d(TAG, "upload started broadcast sent")
val intent = Intent(UPLOAD_STARTED).apply {
putExtra(FileUploadWorker.EXTRA_REMOTE_PATH, upload.remotePath) // real remote
putExtra(FileUploadWorker.EXTRA_OLD_FILE_PATH, upload.originalStoragePath)
putExtra(FileUploadWorker.ACCOUNT_NAME, upload.user.accountName)
setPackage(context.packageName)
}
broadcastManager.sendBroadcast(intent)
}

/**
* Sends a broadcast indicating that an upload has finished, either
* successfully or with an error.
*
* ### Triggered when
* - [UploadFileOperation] completes execution
*
* ### Observed by
* - [com.owncloud.android.ui.activity.FileDisplayActivity.UploadFinishReceiver]
* - [com.owncloud.android.ui.activity.UploadListActivity.UploadFinishReceiver]
* - [com.owncloud.android.ui.preview.PreviewImageActivity.UploadFinishReceiver]
*
*/
fun sendFinished(
upload: UploadFileOperation,
uploadResult: RemoteOperationResult<*>,
unlinkedFromRemotePath: String?,
context: Context
) {
Log_OC.d(TAG, "upload finished broadcast sent")
val intent = Intent(UPLOAD_FINISHED).apply {
// real remote path, after possible automatic renaming
putExtra(FileUploadWorker.EXTRA_REMOTE_PATH, upload.remotePath)
if (upload.wasRenamed()) {
upload.oldFile?.let {
putExtra(FileUploadWorker.EXTRA_OLD_REMOTE_PATH, it.remotePath)
}
}
putExtra(FileUploadWorker.EXTRA_OLD_FILE_PATH, upload.originalStoragePath)
putExtra(FileUploadWorker.ACCOUNT_NAME, upload.user.accountName)
putExtra(FileUploadWorker.EXTRA_UPLOAD_RESULT, uploadResult.isSuccess)
if (unlinkedFromRemotePath != null) {
putExtra(FileUploadWorker.EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath)
}
setPackage(context.packageName)
}
broadcastManager.sendBroadcast(intent)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import com.nextcloud.client.jobs.BackgroundJobManagerImpl
import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager
import com.nextcloud.client.network.ConnectivityService
import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.model.WorkerState
import com.nextcloud.model.WorkerStateObserver
import com.nextcloud.utils.ForegroundServiceHelper
import com.nextcloud.utils.extensions.getPercent
import com.nextcloud.utils.extensions.updateStatus
Expand Down Expand Up @@ -77,10 +75,6 @@ class FileUploadWorker(

var currentUploadFileOperation: UploadFileOperation? = null

private const val UPLOADS_ADDED_MESSAGE = "UPLOADS_ADDED"
private const val UPLOAD_START_MESSAGE = "UPLOAD_START"
private const val UPLOAD_FINISH_MESSAGE = "UPLOAD_FINISH"

private const val BATCH_SIZE = 100

const val EXTRA_UPLOAD_RESULT = "RESULT"
Expand All @@ -96,12 +90,6 @@ class FileUploadWorker(
const val LOCAL_BEHAVIOUR_FORGET = 2
const val LOCAL_BEHAVIOUR_DELETE = 3

fun getUploadsAddedMessage(): String = FileUploadWorker::class.java.name + UPLOADS_ADDED_MESSAGE

fun getUploadStartMessage(): String = FileUploadWorker::class.java.name + UPLOAD_START_MESSAGE

fun getUploadFinishMessage(): String = FileUploadWorker::class.java.name + UPLOAD_FINISH_MESSAGE

fun cancelCurrentUpload(remotePath: String, accountName: String, onCompleted: () -> Unit) {
currentUploadFileOperation?.let {
if (it.remotePath == remotePath && it.user.accountName == accountName) {
Expand Down Expand Up @@ -131,7 +119,7 @@ class FileUploadWorker(
private val notificationId = Random.nextInt()
private val notificationManager = UploadNotificationManager(context, viewThemeUtils, notificationId)
private val intents = FileUploaderIntents(context)
private val fileUploaderDelegate = FileUploaderDelegate()
private val fileUploadBroadcastManager = FileUploadBroadcastManager(localBroadcastManager)

override suspend fun doWork(): Result = try {
Log_OC.d(TAG, "FileUploadWorker started")
Expand All @@ -143,14 +131,15 @@ class FileUploadWorker(
val result = uploadFiles()
backgroundJobManager.logEndOfWorker(workerName, result)
notificationManager.dismissNotification()
if (result == Result.success()) {
setIdleWorkerState()
}
result
} catch (t: Throwable) {
Log_OC.e(TAG, "Error caught at FileUploadWorker $t")
cleanup()
Log_OC.e(TAG, "exception $t")
currentUploadFileOperation?.cancel(null)
Result.failure()
} finally {
// Ensure all database operations are complete before signaling completion
uploadsStorageManager.notifyObserversNow()
notificationManager.dismissNotification()
}

private suspend fun trySetForeground() {
Expand Down Expand Up @@ -198,22 +187,6 @@ class FileUploadWorker(
.setSilent(true)
.build()

private fun cleanup() {
Log_OC.e(TAG, "FileUploadWorker stopped")

setIdleWorkerState()
currentUploadFileOperation?.cancel(null)
notificationManager.dismissNotification()
}

private fun setWorkerState(user: User?) {
WorkerStateObserver.send(WorkerState.FileUploadStarted(user))
}

private fun setIdleWorkerState() {
WorkerStateObserver.send(WorkerState.FileUploadCompleted(currentUploadFileOperation?.file))
}

@Suppress("ReturnCount", "LongMethod", "DEPRECATION")
private suspend fun uploadFiles(): Result = withContext(Dispatchers.IO) {
val accountName = inputData.getString(ACCOUNT)
Expand Down Expand Up @@ -269,7 +242,7 @@ class FileUploadWorker(
return@withContext Result.failure()
}

setWorkerState(user)
fileUploadBroadcastManager.sendAdded(context)
val operation = createUploadFileOperation(upload, user)
currentUploadFileOperation = operation

Expand Down Expand Up @@ -307,17 +280,18 @@ class FileUploadWorker(
operation: UploadFileOperation,
result: RemoteOperationResult<*>
) {
val isLastUpload = currentUploadIndex == totalUploadSize

val shouldBroadcast =
(totalUploadSize > BATCH_SIZE && currentUploadIndex > 0) && currentUploadIndex % BATCH_SIZE == 0
(currentUploadIndex % BATCH_SIZE == 0 && totalUploadSize > BATCH_SIZE) ||
isLastUpload

if (shouldBroadcast) {
// delay broadcast
fileUploaderDelegate.sendBroadcastUploadFinished(
fileUploadBroadcastManager.sendFinished(
operation,
result,
operation.oldFile?.storagePath,
context,
localBroadcastManager
context
)
}
}
Expand Down Expand Up @@ -369,6 +343,7 @@ class FileUploadWorker(
val file = File(operation.originalStoragePath)
val remoteId: String? = operation.file.remoteId
task.execute(ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, remoteId))
fileUploadBroadcastManager.sendStarted(operation, context)
} catch (e: Exception) {
Log_OC.e(TAG, "Error uploading", e)
result = RemoteOperationResult<Any?>(e)
Expand Down
Loading
Loading