Skip to content

Commit 86d2bdf

Browse files
Merge pull request nextcloud#16394 from nextcloud/backport/16317/stable-3.35
[stable-3.35] fix(receivers): upload
2 parents 48588c3 + bacae96 commit 86d2bdf

11 files changed

Lines changed: 184 additions & 226 deletions

File tree

app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ import com.nextcloud.client.integrations.deck.DeckApi
2525
import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker
2626
import com.nextcloud.client.jobs.autoUpload.FileSystemRepository
2727
import com.nextcloud.client.jobs.download.FileDownloadWorker
28+
import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker
2829
import com.nextcloud.client.jobs.metadata.MetadataWorker
2930
import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker
30-
import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker
3131
import com.nextcloud.client.jobs.upload.FileUploadWorker
3232
import com.nextcloud.client.logger.Logger
3333
import com.nextcloud.client.network.ConnectivityService
@@ -180,7 +180,8 @@ class BackgroundJobFactory @Inject constructor(
180180
syncedFolderProvider = syncedFolderProvider,
181181
backgroundJobManager = backgroundJobManager.get(),
182182
repository = FileSystemRepository(dao = database.fileSystemDao(), context),
183-
viewThemeUtils = viewThemeUtils.get()
183+
viewThemeUtils = viewThemeUtils.get(),
184+
localBroadcastManager = localBroadcastManager.get()
184185
)
185186

186187
private fun createOfflineSyncWork(context: Context, params: WorkerParameters): OfflineSyncWork = OfflineSyncWork(

app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import android.app.Notification
1111
import android.content.Context
1212
import android.content.res.Resources
1313
import androidx.exifinterface.media.ExifInterface
14+
import androidx.localbroadcastmanager.content.LocalBroadcastManager
1415
import androidx.work.CoroutineWorker
1516
import androidx.work.ForegroundInfo
1617
import androidx.work.WorkerParameters
@@ -21,6 +22,7 @@ import com.nextcloud.client.database.entity.toOCUpload
2122
import com.nextcloud.client.database.entity.toUploadEntity
2223
import com.nextcloud.client.device.PowerManagementService
2324
import com.nextcloud.client.jobs.BackgroundJobManager
25+
import com.nextcloud.client.jobs.upload.FileUploadBroadcastManager
2426
import com.nextcloud.client.jobs.upload.FileUploadWorker
2527
import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager
2628
import com.nextcloud.client.network.ConnectivityService
@@ -63,7 +65,8 @@ class AutoUploadWorker(
6365
private val syncedFolderProvider: SyncedFolderProvider,
6466
private val backgroundJobManager: BackgroundJobManager,
6567
private val repository: FileSystemRepository,
66-
val viewThemeUtils: ViewThemeUtils
68+
val viewThemeUtils: ViewThemeUtils,
69+
localBroadcastManager: LocalBroadcastManager
6770
) : CoroutineWorker(context, params) {
6871

6972
companion object {
@@ -75,6 +78,7 @@ class AutoUploadWorker(
7578
}
7679

7780
private val helper = AutoUploadHelper()
81+
private val fileUploadBroadcastManager = FileUploadBroadcastManager(localBroadcastManager)
7882
private lateinit var syncedFolder: SyncedFolder
7983
private val notificationManager = AutoUploadNotificationManager(context, viewThemeUtils, NOTIFICATION_ID)
8084

@@ -282,6 +286,7 @@ class AutoUploadWorker(
282286
updateNotification()
283287

284288
var lastId = 0
289+
285290
while (true) {
286291
val filePathsWithIds = repository.getFilePathsWithIds(syncedFolder, lastId)
287292

@@ -291,7 +296,7 @@ class AutoUploadWorker(
291296
}
292297
Log_OC.d(TAG, "Processing batch: lastId=$lastId, count=${filePathsWithIds.size}")
293298

294-
filePathsWithIds.forEach { (path, id) ->
299+
filePathsWithIds.forEachIndexed { batchIndex, (path, id) ->
295300
val file = File(path)
296301
val localPath = file.absolutePath
297302
val remotePath = getRemotePath(
@@ -326,10 +331,12 @@ class AutoUploadWorker(
326331
uploadEntity = uploadEntity.copy(id = generatedId.toInt())
327332
upload.uploadId = generatedId
328333

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

332338
val result = operation.execute(client)
339+
fileUploadBroadcastManager.sendStarted(operation, context)
333340
uploadsStorageManager.updateStatus(uploadEntity, result.isSuccess)
334341

335342
UploadErrorNotificationManager.handleResult(
@@ -354,6 +361,11 @@ class AutoUploadWorker(
354361
Log_OC.w(TAG, "Marked CONFLICT file as handled: $localPath")
355362
}
356363
}
364+
365+
val isLastInBatch = (batchIndex == filePathsWithIds.size - 1)
366+
if (isLastInBatch) {
367+
sendUploadFinishEvent(operation, result)
368+
}
357369
} catch (e: Exception) {
358370
uploadsStorageManager.updateStatus(
359371
uploadEntity,
@@ -524,4 +536,13 @@ class AutoUploadWorker(
524536
}
525537
return lastModificationTime
526538
}
539+
540+
private fun sendUploadFinishEvent(operation: UploadFileOperation, result: RemoteOperationResult<*>) {
541+
fileUploadBroadcastManager.sendFinished(
542+
operation,
543+
result,
544+
operation.oldFile?.storagePath,
545+
context
546+
)
547+
}
527548
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH
6+
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
7+
*/
8+
package com.nextcloud.client.jobs.upload
9+
10+
import android.content.Context
11+
import android.content.Intent
12+
import androidx.localbroadcastmanager.content.LocalBroadcastManager
13+
import com.owncloud.android.lib.common.operations.RemoteOperationResult
14+
import com.owncloud.android.lib.common.utils.Log_OC
15+
import com.owncloud.android.operations.UploadFileOperation
16+
17+
/**
18+
* Manages local broadcasts related to file upload lifecycle events.
19+
*
20+
* This class is responsible for notifying components about upload
21+
* queue changes and upload state transitions (added, started, finished).
22+
*
23+
* All broadcasts are sent via [LocalBroadcastManager].
24+
*/
25+
class FileUploadBroadcastManager(private val broadcastManager: LocalBroadcastManager) {
26+
27+
companion object {
28+
private const val TAG = "📣" + "FileUploadBroadcastManager"
29+
30+
const val UPLOAD_ADDED = "UPLOAD_ADDED"
31+
const val UPLOAD_STARTED = "UPLOAD_STARTED"
32+
const val UPLOAD_FINISHED = "UPLOAD_FINISHED"
33+
}
34+
35+
/**
36+
* Sends a broadcast to indicate that an upload has been added to the database.
37+
*
38+
* ### Triggered when
39+
* - [UploadFileOperation] added
40+
*
41+
* ### Observed by
42+
* - [com.owncloud.android.ui.activity.UploadListActivity.UploadFinishReceiver]
43+
*
44+
*/
45+
fun sendAdded(context: Context) {
46+
Log_OC.d(TAG, "upload added broadcast sent")
47+
val intent = Intent(UPLOAD_ADDED).apply {
48+
setPackage(context.packageName)
49+
}
50+
broadcastManager.sendBroadcast(intent)
51+
}
52+
53+
/**
54+
* Sends a broadcast indicating that an upload started.
55+
*
56+
* ### Triggered when
57+
* - [UploadFileOperation] started
58+
*
59+
* ### Observed by
60+
* - [com.owncloud.android.ui.activity.UploadListActivity.UploadFinishReceiver]
61+
*
62+
*/
63+
fun sendStarted(upload: UploadFileOperation, context: Context) {
64+
Log_OC.d(TAG, "upload started broadcast sent")
65+
val intent = Intent(UPLOAD_STARTED).apply {
66+
putExtra(FileUploadWorker.EXTRA_REMOTE_PATH, upload.remotePath) // real remote
67+
putExtra(FileUploadWorker.EXTRA_OLD_FILE_PATH, upload.originalStoragePath)
68+
putExtra(FileUploadWorker.ACCOUNT_NAME, upload.user.accountName)
69+
setPackage(context.packageName)
70+
}
71+
broadcastManager.sendBroadcast(intent)
72+
}
73+
74+
/**
75+
* Sends a broadcast indicating that an upload has finished, either
76+
* successfully or with an error.
77+
*
78+
* ### Triggered when
79+
* - [UploadFileOperation] completes execution
80+
*
81+
* ### Observed by
82+
* - [com.owncloud.android.ui.activity.FileDisplayActivity.UploadFinishReceiver]
83+
* - [com.owncloud.android.ui.activity.UploadListActivity.UploadFinishReceiver]
84+
* - [com.owncloud.android.ui.preview.PreviewImageActivity.UploadFinishReceiver]
85+
*
86+
*/
87+
fun sendFinished(
88+
upload: UploadFileOperation,
89+
uploadResult: RemoteOperationResult<*>,
90+
unlinkedFromRemotePath: String?,
91+
context: Context
92+
) {
93+
Log_OC.d(TAG, "upload finished broadcast sent")
94+
val intent = Intent(UPLOAD_FINISHED).apply {
95+
// real remote path, after possible automatic renaming
96+
putExtra(FileUploadWorker.EXTRA_REMOTE_PATH, upload.remotePath)
97+
if (upload.wasRenamed()) {
98+
upload.oldFile?.let {
99+
putExtra(FileUploadWorker.EXTRA_OLD_REMOTE_PATH, it.remotePath)
100+
}
101+
}
102+
putExtra(FileUploadWorker.EXTRA_OLD_FILE_PATH, upload.originalStoragePath)
103+
putExtra(FileUploadWorker.ACCOUNT_NAME, upload.user.accountName)
104+
putExtra(FileUploadWorker.EXTRA_UPLOAD_RESULT, uploadResult.isSuccess)
105+
if (unlinkedFromRemotePath != null) {
106+
putExtra(FileUploadWorker.EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath)
107+
}
108+
setPackage(context.packageName)
109+
}
110+
broadcastManager.sendBroadcast(intent)
111+
}
112+
}

app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import com.nextcloud.client.jobs.BackgroundJobManagerImpl
2222
import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager
2323
import com.nextcloud.client.network.ConnectivityService
2424
import com.nextcloud.client.preferences.AppPreferences
25-
import com.nextcloud.model.WorkerState
26-
import com.nextcloud.model.WorkerStateObserver
2725
import com.nextcloud.utils.ForegroundServiceHelper
2826
import com.nextcloud.utils.extensions.getPercent
2927
import com.nextcloud.utils.extensions.updateStatus
@@ -77,10 +75,6 @@ class FileUploadWorker(
7775

7876
var currentUploadFileOperation: UploadFileOperation? = null
7977

80-
private const val UPLOADS_ADDED_MESSAGE = "UPLOADS_ADDED"
81-
private const val UPLOAD_START_MESSAGE = "UPLOAD_START"
82-
private const val UPLOAD_FINISH_MESSAGE = "UPLOAD_FINISH"
83-
8478
private const val BATCH_SIZE = 100
8579

8680
const val EXTRA_UPLOAD_RESULT = "RESULT"
@@ -96,12 +90,6 @@ class FileUploadWorker(
9690
const val LOCAL_BEHAVIOUR_FORGET = 2
9791
const val LOCAL_BEHAVIOUR_DELETE = 3
9892

99-
fun getUploadsAddedMessage(): String = FileUploadWorker::class.java.name + UPLOADS_ADDED_MESSAGE
100-
101-
fun getUploadStartMessage(): String = FileUploadWorker::class.java.name + UPLOAD_START_MESSAGE
102-
103-
fun getUploadFinishMessage(): String = FileUploadWorker::class.java.name + UPLOAD_FINISH_MESSAGE
104-
10593
fun cancelCurrentUpload(remotePath: String, accountName: String, onCompleted: () -> Unit) {
10694
currentUploadFileOperation?.let {
10795
if (it.remotePath == remotePath && it.user.accountName == accountName) {
@@ -131,7 +119,7 @@ class FileUploadWorker(
131119
private val notificationId = Random.nextInt()
132120
private val notificationManager = UploadNotificationManager(context, viewThemeUtils, notificationId)
133121
private val intents = FileUploaderIntents(context)
134-
private val fileUploaderDelegate = FileUploaderDelegate()
122+
private val fileUploadBroadcastManager = FileUploadBroadcastManager(localBroadcastManager)
135123

136124
override suspend fun doWork(): Result = try {
137125
Log_OC.d(TAG, "FileUploadWorker started")
@@ -143,14 +131,15 @@ class FileUploadWorker(
143131
val result = uploadFiles()
144132
backgroundJobManager.logEndOfWorker(workerName, result)
145133
notificationManager.dismissNotification()
146-
if (result == Result.success()) {
147-
setIdleWorkerState()
148-
}
149134
result
150135
} catch (t: Throwable) {
151-
Log_OC.e(TAG, "Error caught at FileUploadWorker $t")
152-
cleanup()
136+
Log_OC.e(TAG, "exception $t")
137+
currentUploadFileOperation?.cancel(null)
153138
Result.failure()
139+
} finally {
140+
// Ensure all database operations are complete before signaling completion
141+
uploadsStorageManager.notifyObserversNow()
142+
notificationManager.dismissNotification()
154143
}
155144

156145
private suspend fun trySetForeground() {
@@ -198,22 +187,6 @@ class FileUploadWorker(
198187
.setSilent(true)
199188
.build()
200189

201-
private fun cleanup() {
202-
Log_OC.e(TAG, "FileUploadWorker stopped")
203-
204-
setIdleWorkerState()
205-
currentUploadFileOperation?.cancel(null)
206-
notificationManager.dismissNotification()
207-
}
208-
209-
private fun setWorkerState(user: User?) {
210-
WorkerStateObserver.send(WorkerState.FileUploadStarted(user))
211-
}
212-
213-
private fun setIdleWorkerState() {
214-
WorkerStateObserver.send(WorkerState.FileUploadCompleted(currentUploadFileOperation?.file))
215-
}
216-
217190
@Suppress("ReturnCount", "LongMethod", "DEPRECATION")
218191
private suspend fun uploadFiles(): Result = withContext(Dispatchers.IO) {
219192
val accountName = inputData.getString(ACCOUNT)
@@ -269,7 +242,7 @@ class FileUploadWorker(
269242
return@withContext Result.failure()
270243
}
271244

272-
setWorkerState(user)
245+
fileUploadBroadcastManager.sendAdded(context)
273246
val operation = createUploadFileOperation(upload, user)
274247
currentUploadFileOperation = operation
275248

@@ -307,17 +280,18 @@ class FileUploadWorker(
307280
operation: UploadFileOperation,
308281
result: RemoteOperationResult<*>
309282
) {
283+
val isLastUpload = currentUploadIndex == totalUploadSize
284+
310285
val shouldBroadcast =
311-
(totalUploadSize > BATCH_SIZE && currentUploadIndex > 0) && currentUploadIndex % BATCH_SIZE == 0
286+
(currentUploadIndex % BATCH_SIZE == 0 && totalUploadSize > BATCH_SIZE) ||
287+
isLastUpload
312288

313289
if (shouldBroadcast) {
314-
// delay broadcast
315-
fileUploaderDelegate.sendBroadcastUploadFinished(
290+
fileUploadBroadcastManager.sendFinished(
316291
operation,
317292
result,
318293
operation.oldFile?.storagePath,
319-
context,
320-
localBroadcastManager
294+
context
321295
)
322296
}
323297
}
@@ -369,6 +343,7 @@ class FileUploadWorker(
369343
val file = File(operation.originalStoragePath)
370344
val remoteId: String? = operation.file.remoteId
371345
task.execute(ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, remoteId))
346+
fileUploadBroadcastManager.sendStarted(operation, context)
372347
} catch (e: Exception) {
373348
Log_OC.e(TAG, "Error uploading", e)
374349
result = RemoteOperationResult<Any?>(e)

0 commit comments

Comments
 (0)