@@ -9,6 +9,7 @@ package com.nextcloud.client.jobs.upload
99
1010import android.app.Notification
1111import android.content.Context
12+ import androidx.core.app.NotificationCompat
1213import androidx.localbroadcastmanager.content.LocalBroadcastManager
1314import androidx.work.CoroutineWorker
1415import androidx.work.ForegroundInfo
@@ -18,6 +19,7 @@ import com.nextcloud.client.account.UserAccountManager
1819import com.nextcloud.client.device.PowerManagementService
1920import com.nextcloud.client.jobs.BackgroundJobManager
2021import com.nextcloud.client.jobs.BackgroundJobManagerImpl
22+ import com.nextcloud.client.jobs.upload.FileUploadWorker.Companion.SHOW_SAME_FILE_ALREADY_EXISTS_NOTIFICATION
2123import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager
2224import com.nextcloud.client.network.ConnectivityService
2325import com.nextcloud.client.preferences.AppPreferences
@@ -39,11 +41,13 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCo
3941import com.owncloud.android.lib.common.utils.Log_OC
4042import com.owncloud.android.operations.UploadFileOperation
4143import com.owncloud.android.operations.albums.CopyFileToAlbumOperation
44+ import com.owncloud.android.ui.notifications.NotificationUtils
4245import com.owncloud.android.utils.theme.ViewThemeUtils
4346import kotlinx.coroutines.Dispatchers
4447import kotlinx.coroutines.ensureActive
4548import kotlinx.coroutines.withContext
4649import java.io.File
50+ import java.util.concurrent.ConcurrentHashMap
4751import kotlin.random.Random
4852
4953/* *
@@ -68,16 +72,43 @@ class AlbumFileUploadWorker(
6872 companion object {
6973 val TAG : String = AlbumFileUploadWorker ::class .java.simpleName
7074
71- var currentUploadFileOperation: UploadFileOperation ? = null
72-
73- private const val BATCH_SIZE = 100
74-
7575 const val ALBUM_NAME = " album_name"
7676
7777 const val ACCOUNT = " data_account"
7878 const val UPLOAD_IDS = " uploads_ids"
7979 const val CURRENT_BATCH_INDEX = " batch_index"
8080 const val TOTAL_UPLOAD_SIZE = " total_upload_size"
81+ private const val BATCH_SIZE = 100
82+
83+ private val activeOperations = ConcurrentHashMap <Long , UploadFileOperation >()
84+
85+ @JvmOverloads
86+ fun cancelUpload (remotePath : String? , accountName : String? , onCompleted : () -> Unit = {}) {
87+ val operation =
88+ activeOperations.values.find { it.remotePath == remotePath && it.user.accountName == accountName }
89+
90+ operation?.let {
91+ Log_OC .d(TAG , " upload operation is cancelled: $remotePath " )
92+ operation.cancel(ResultCode .USER_CANCELLED )
93+ activeOperations.remove(operation.ocUploadId)
94+ }
95+
96+ onCompleted()
97+ }
98+
99+ suspend fun cancelUploads (remotePaths : List <String >, accountName : String ) {
100+ withContext(Dispatchers .IO ) {
101+ remotePaths.forEach {
102+ cancelUpload(it, accountName)
103+ }
104+ }
105+ }
106+
107+ fun getCurrentUpload (id : Long? ): UploadFileOperation ? = activeOperations[id]
108+
109+ fun isUploading (remotePath : String? , accountName : String? ): Boolean = activeOperations.values.any {
110+ it.remotePath == remotePath && it.user.accountName == accountName
111+ }
81112 }
82113
83114 private var lastPercent = 0
@@ -87,19 +118,20 @@ class AlbumFileUploadWorker(
87118 private val fileUploadEventBroadcaster = FileUploadEventBroadcaster (localBroadcastManager)
88119
89120 override suspend fun doWork (): Result = try {
121+ trySetForeground()
122+
90123 Log_OC .d(TAG , " AlbumFileUploadWorker started" )
91124 val workerName = BackgroundJobManagerImpl .formatClassTag(this ::class )
92125 backgroundJobManager.logStartOfWorker(workerName)
93126
94- trySetForeground()
95-
96127 val result = uploadFiles()
97128 backgroundJobManager.logEndOfWorker(workerName, result)
98129 notificationManager.dismissNotification()
99130 result
100131 } catch (t: Throwable ) {
101132 Log_OC .e(TAG , " exception $t " )
102- currentUploadFileOperation?.cancel(null )
133+ activeOperations.values.forEach { it.cancel(null ) }
134+ activeOperations.clear()
103135 Result .failure()
104136 } finally {
105137 // Ensure all database operations are complete before signaling completion
@@ -111,8 +143,7 @@ class AlbumFileUploadWorker(
111143 try {
112144 val notificationTitle = notificationManager.currentOperationTitle
113145 ? : context.getString(R .string.foreground_service_upload)
114-
115- val notification = notificationManager.createSilentNotification(notificationTitle, R .drawable.uploads)
146+ val notification = createNotification(notificationTitle)
116147 updateForegroundInfo(notification)
117148 } catch (e: Exception ) {
118149 // Continue without foreground service - uploads will still work
@@ -123,7 +154,7 @@ class AlbumFileUploadWorker(
123154 override suspend fun getForegroundInfo (): ForegroundInfo {
124155 val notificationTitle = notificationManager.currentOperationTitle
125156 ? : context.getString(R .string.foreground_service_upload)
126- val notification = notificationManager.createSilentNotification (notificationTitle, R .drawable.uploads )
157+ val notification = createNotification (notificationTitle)
127158
128159 return ForegroundServiceHelper .createWorkerForegroundInfo(
129160 notificationId,
@@ -141,6 +172,18 @@ class AlbumFileUploadWorker(
141172 setForeground(foregroundInfo)
142173 }
143174
175+ private fun createNotification (title : String ): Notification =
176+ NotificationCompat .Builder (context, NotificationUtils .NOTIFICATION_CHANNEL_UPLOAD )
177+ .setContentTitle(title)
178+ .setSmallIcon(R .drawable.uploads)
179+ .setOngoing(true )
180+ .setSound(null )
181+ .setVibrate(null )
182+ .setOnlyAlertOnce(true )
183+ .setPriority(NotificationCompat .PRIORITY_LOW )
184+ .setSilent(true )
185+ .build()
186+
144187 @Suppress(" ReturnCount" , " LongMethod" , " DEPRECATION" )
145188 private suspend fun uploadFiles (): Result = withContext(Dispatchers .IO ) {
146189 val accountName = inputData.getString(ACCOUNT )
@@ -204,7 +247,7 @@ class AlbumFileUploadWorker(
204247
205248 fileUploadEventBroadcaster.sendUploadEnqueued(context)
206249 val operation = createUploadFileOperation(upload, user)
207- currentUploadFileOperation = operation
250+ activeOperations[upload.uploadId] = operation
208251
209252 val currentIndex = (index + 1 )
210253 val currentUploadIndex = (currentIndex + previouslyUploadedFileSize)
@@ -216,11 +259,9 @@ class AlbumFileUploadWorker(
216259 )
217260
218261 val result = withContext(Dispatchers .IO ) {
219- upload(operation, albumName, user, client)
262+ upload(upload, operation, albumName, user, client)
220263 }
221- val entity = uploadsStorageManager.uploadDao.getUploadById(upload.uploadId, accountName)
222- uploadsStorageManager.updateStatus(entity, result.isSuccess)
223- currentUploadFileOperation = null
264+ activeOperations.remove(upload.uploadId)
224265
225266 if (result.code == ResultCode .QUOTA_EXCEEDED ) {
226267 Log_OC .w(TAG , " Quota exceeded, stopping uploads" )
@@ -289,6 +330,7 @@ class AlbumFileUploadWorker(
289330
290331 @Suppress(" TooGenericExceptionCaught" , " DEPRECATION" )
291332 private suspend fun upload (
333+ upload : OCUpload ,
292334 operation : UploadFileOperation ,
293335 albumName : String ,
294336 user : User ,
@@ -314,35 +356,43 @@ class AlbumFileUploadWorker(
314356 fileUploadEventBroadcaster.sendUploadStarted(operation, context)
315357 } catch (e: Exception ) {
316358 Log_OC .e(TAG , " Error uploading" , e)
317- result = RemoteOperationResult (e)
318- } finally {
319- if (! isStopped) {
320- uploadsStorageManager.updateDatabaseUploadResult(result, operation)
321- // resolving file conflict will trigger normal file upload and shows two upload process
322- // one for normal and one for Album upload
323- // as customizing conflict can break normal upload
324- // so we are removing the upload if it's a conflict
325- // Note: this is fallback logic because default policy while uploading is RENAME
326- // if in some case code reach here it will remove the upload
327- // so we are checking it first and removing the upload
328- if (result.code == ResultCode .SYNC_CONFLICT ) {
329- uploadsStorageManager.removeUpload(
330- operation.user.accountName,
331- operation.remotePath
359+ uploadsStorageManager.run {
360+ uploadDao.getUploadById(upload.uploadId, user.accountName)?.let { entity ->
361+ updateStatus(
362+ entity,
363+ UploadsStorageManager .UploadStatus .UPLOAD_FAILED
332364 )
333- } else {
334- UploadErrorNotificationManager .handleResult(
335- context,
336- notificationManager,
337- operation,
338- result,
339- onSameFileConflict = {
340- withContext(Dispatchers .Main ) {
365+ }
366+ }
367+ result = RemoteOperationResult (e)
368+ }
369+
370+ if (! isStopped) {
371+ // resolving file conflict will trigger normal file upload and shows two upload process
372+ // one for normal and one for Album upload
373+ // as customizing conflict can break normal upload
374+ // so we are removing the upload if it's a conflict
375+ // Note: this is fallback logic because default policy while uploading is RENAME
376+ // if in some case code reach here it will remove the upload
377+ // so we are checking it first and removing the upload
378+ if (result.code == ResultCode .SYNC_CONFLICT ) {
379+ activeOperations.remove(upload.uploadId)
380+ } else {
381+ UploadErrorNotificationManager .handleResult(
382+ context,
383+ notificationManager,
384+ operation,
385+ result,
386+ onSameFileConflict = {
387+ withContext(Dispatchers .Main ) {
388+ val showSameFileAlreadyExistsNotification =
389+ inputData.getBoolean(SHOW_SAME_FILE_ALREADY_EXISTS_NOTIFICATION , false )
390+ if (showSameFileAlreadyExistsNotification) {
341391 notificationManager.showSameFileAlreadyExistsNotification(operation.fileName)
342392 }
343393 }
344- )
345- }
394+ }
395+ )
346396 }
347397 }
348398
@@ -368,6 +418,9 @@ class AlbumFileUploadWorker(
368418
369419 if (percent != lastPercent && (currentTime - lastUpdateTime) >= minProgressUpdateInterval) {
370420 notificationManager.run {
421+ val currentUploadFileOperation =
422+ activeOperations.values.find { it.originalStoragePath == fileAbsoluteName }
423+
371424 val accountName = currentUploadFileOperation?.user?.accountName
372425 val remotePath = currentUploadFileOperation?.remotePath
373426
@@ -376,7 +429,7 @@ class AlbumFileUploadWorker(
376429 if (accountName != null && remotePath != null ) {
377430 val key: String = FileUploadHelper .buildRemoteName(accountName, remotePath)
378431 val boundListener = FileUploadHelper .mBoundListeners[key]
379- val filename = currentUploadFileOperation? .fileName ? : " "
432+ val filename = currentUploadFileOperation.fileName ? : " "
380433
381434 boundListener?.onTransferProgress(
382435 progressRate,
0 commit comments