@@ -34,8 +34,6 @@ import com.owncloud.android.operations.RenameFileOperation
3434import com.owncloud.android.utils.MimeTypeUtil
3535import com.owncloud.android.utils.theme.ViewThemeUtils
3636import kotlinx.coroutines.Dispatchers
37- import kotlinx.coroutines.NonCancellable
38- import kotlinx.coroutines.delay
3937import kotlinx.coroutines.withContext
4038import kotlin.coroutines.resume
4139import kotlin.coroutines.suspendCoroutine
@@ -62,55 +60,106 @@ class OfflineOperationsWorker(
6260 private val notificationManager = OfflineOperationsNotificationManager (context, viewThemeUtils)
6361 private var repository = OfflineOperationsRepository (fileDataStorageManager)
6462
63+ @Suppress(" TooGenericExceptionCaught" )
6564 override suspend fun doWork (): Result = withContext(Dispatchers .IO ) {
66- val jobName = inputData.getString( JOB_NAME )
67- Log_OC .d( TAG , " [ $jobName ] OfflineOperationsWorker started for user: ${user.accountName} " )
68-
69- if ( ! isNetworkAndServerAvailable()) {
70- Log_OC .w( TAG , " ⚠️ No internet/server connection. Retrying later... " )
71- return @withContext Result .retry()
72- }
73-
74- val client = clientFactory.create(user)
65+ try {
66+ val jobName = inputData.getString( JOB_NAME )
67+ Log_OC .d( TAG , " [ $jobName ] OfflineOperationsWorker started for user: ${user.accountName} " )
68+
69+ // check network connection
70+ if ( ! isNetworkAndServerAvailable()) {
71+ Log_OC .w( TAG , " ⚠️ No internet/server connection. Retrying later... " )
72+ return @withContext Result .retry()
73+ }
7574
76- notificationManager.start()
77- val operations = fileDataStorageManager.offlineOperationDao.getAll()
78- Log_OC .d(TAG , " 📋 Found ${operations.size} offline operations to process" )
75+ // check offline operations
76+ val operations = fileDataStorageManager.offlineOperationDao.getAll()
77+ if (operations.isEmpty()) {
78+ Log_OC .d(TAG , " Skipping, no offline operation found" )
79+ return @withContext Result .success()
80+ }
7981
80- val result = processOperations(operations, client)
81- notificationManager.dismissNotification()
82+ // process offline operations
83+ notificationManager.start()
84+ val client = clientFactory.create(user)
85+ processOperations(operations, client)
8286
83- Log_OC .d(TAG , " 🏁 Worker finished with result: $result " )
84- return @withContext result
87+ // finish
88+ WorkerStateLiveData .instance().setWorkState(WorkerState .OfflineOperationsCompleted )
89+ Log_OC .d(TAG , " 🏁 Worker finished with result" )
90+ return @withContext Result .success()
91+ } catch (e: Exception ) {
92+ Log_OC .e(TAG , " 💥 ProcessOperations failed: ${e.message} " )
93+ return @withContext Result .failure()
94+ } finally {
95+ notificationManager.dismissNotification()
96+ }
8597 }
8698
99+ // region Handle offline operations
87100 @Suppress(" TooGenericExceptionCaught" )
88- private suspend fun processOperations (operations : List <OfflineOperationEntity >, client : OwnCloudClient ): Result {
101+ private suspend fun processOperations (operations : List <OfflineOperationEntity >, client : OwnCloudClient ) {
89102 val totalOperationSize = operations.size
103+ operations.forEachIndexed { index, operation ->
104+ try {
105+ Log_OC .d(TAG , " Processing operation, path: ${operation.path} " )
106+ val result = executeOperation(operation, client)
107+ handleResult(operation, totalOperationSize, index, result)
108+ } catch (e: Exception ) {
109+ Log_OC .e(TAG , " 💥 Exception while processing operation id=${operation.id} : ${e.message} " )
110+ }
111+ }
112+ }
90113
91- return try {
92- operations.forEachIndexed { index, operation ->
93- try {
94- Log_OC .d(TAG , " Processing operation, path: ${operation.path} " )
95- val result = executeOperation(operation, client)
96- val success = handleResult(operation, totalOperationSize, index, result)
97-
98- if (! success) {
99- Log_OC .e(TAG , " ❌ Operation failed: id=${operation.id} , type=${operation.type} " )
100- }
101- } catch (e: Exception ) {
102- Log_OC .e(TAG , " 💥 Exception while processing operation id=${operation.id} : ${e.message} " )
103- }
114+ private fun handleResult (
115+ operation : OfflineOperationEntity ,
116+ totalOperations : Int ,
117+ currentSuccessfulOperationIndex : Int ,
118+ result : OfflineOperationResult
119+ ) {
120+ val operationResult = result?.first ? : return
121+ val logMessage = if (operationResult.isSuccess) " Operation completed" else " Operation failed"
122+ Log_OC .d(TAG , " $logMessage filename: ${operation.filename} , type: ${operation.type} " )
123+
124+ return if (result.first?.isSuccess == true ) {
125+ handleSuccessResult(operation, totalOperations, currentSuccessfulOperationIndex)
126+ } else {
127+ handleErrorResult(operation.id, result)
128+ }
129+ }
130+
131+ private fun handleSuccessResult (
132+ operation : OfflineOperationEntity ,
133+ totalOperations : Int ,
134+ currentSuccessfulOperationIndex : Int
135+ ) {
136+ if (operation.type is OfflineOperationType .RemoveFile ) {
137+ val operationType = operation.type as OfflineOperationType .RemoveFile
138+ fileDataStorageManager.getFileByDecryptedRemotePath(operationType.path)?.let { ocFile ->
139+ repository.deleteOperation(ocFile)
104140 }
141+ } else {
142+ repository.updateNextOperations(operation)
143+ }
105144
106- Log_OC .i(TAG , " ✅ All offline operations completed successfully." )
107- WorkerStateLiveData .instance().setWorkState(WorkerState .OfflineOperationsCompleted )
108- Result .success()
109- } catch (e: Exception ) {
110- Log_OC .e(TAG , " 💥 ProcessOperations failed: ${e.message} " )
111- Result .failure()
145+ fileDataStorageManager.offlineOperationDao.delete(operation)
146+ notificationManager.update(totalOperations, currentSuccessfulOperationIndex + 1 , operation.filename ? : " " )
147+ }
148+
149+ private fun handleErrorResult (id : Int? , result : OfflineOperationResult ) {
150+ val operationResult = result?.first ? : return
151+ val operation = result.second ? : return
152+ Log_OC .e(TAG , " ❌ Operation failed [id=$id ]: code=${operationResult.code} , message=${operationResult.message} " )
153+ val excludedErrorCodes =
154+ listOf (RemoteOperationResult .ResultCode .FOLDER_ALREADY_EXISTS , RemoteOperationResult .ResultCode .LOCKED )
155+
156+ if (! excludedErrorCodes.contains(operationResult.code)) {
157+ notificationManager.showNewNotification(id, operationResult, operation)
158+ } else {
159+ Log_OC .d(TAG , " ℹ️ Ignored error: ${operationResult.code} " )
112160 }
113161 }
162+ // endregion
114163
115164 private suspend fun isNetworkAndServerAvailable (): Boolean = suspendCoroutine { continuation ->
116165 connectivityService.isNetworkAndServerAvailable { result ->
@@ -119,19 +168,28 @@ class OfflineOperationsWorker(
119168 }
120169
121170 // region Operation Execution
122- @Suppress(" ComplexCondition" )
171+ @Suppress(" ComplexCondition" , " LongMethod " )
123172 private suspend fun executeOperation (
124173 operation : OfflineOperationEntity ,
125174 client : OwnCloudClient
126175 ): OfflineOperationResult ? = withContext(Dispatchers .IO ) {
127- val path = (operation.path)
176+ var path = (operation.path)
128177 if (path == null ) {
129178 Log_OC .w(TAG , " ⚠️ Skipped: path is null for operation id=${operation.id} " )
130179 return @withContext null
131180 }
132181
182+ if (operation.type is OfflineOperationType .CreateFile && path.endsWith(OCFile .PATH_SEPARATOR )) {
183+ Log_OC .w(
184+ TAG ,
185+ " Create file operation should not ends with path separator removing suffix, " +
186+ " operation id=${operation.id} "
187+ )
188+ path = path.removeSuffix(OCFile .PATH_SEPARATOR )
189+ }
190+
133191 val remoteFile = getRemoteFile(path)
134- val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(operation. path)
192+ val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(path)
135193
136194 if (remoteFile != null && ocFile != null && isFileChanged(remoteFile, ocFile)) {
137195 Log_OC .w(TAG , " ⚠️ Conflict detected: File already exists on server. Skipping operation id=${operation.id} " )
@@ -148,6 +206,18 @@ class OfflineOperationsWorker(
148206 return @withContext null
149207 }
150208
209+ if (operation.isRenameOrRemove() && ocFile == null ) {
210+ Log_OC .d(TAG , " Skipping, attempting to delete or rename non-existing file" )
211+ fileDataStorageManager.offlineOperationDao.delete(operation)
212+ return @withContext null
213+ }
214+
215+ if (operation.isCreate() && remoteFile != null && ocFile != null && ! isFileChanged(remoteFile, ocFile)) {
216+ Log_OC .d(TAG , " Skipping, attempting to create same file creation" )
217+ fileDataStorageManager.offlineOperationDao.delete(operation)
218+ return @withContext null
219+ }
220+
151221 return @withContext when (val type = operation.type) {
152222 is OfflineOperationType .CreateFolder -> {
153223 Log_OC .d(TAG , " 📂 Creating folder at ${type.path} " )
@@ -173,113 +243,42 @@ class OfflineOperationsWorker(
173243 }
174244
175245 @Suppress(" DEPRECATION" )
176- private suspend fun createFolder (
177- operation : OfflineOperationEntity ,
178- client : OwnCloudClient
179- ): OfflineOperationResult {
246+ private fun createFolder (operation : OfflineOperationEntity , client : OwnCloudClient ): OfflineOperationResult {
180247 val operationType = (operation.type as OfflineOperationType .CreateFolder )
181- val createFolderOperation = withContext(NonCancellable ) {
182- CreateFolderOperation (operationType.path, user, context, fileDataStorageManager)
183- }
184-
248+ val createFolderOperation = CreateFolderOperation (operationType.path, user, context, fileDataStorageManager)
185249 return createFolderOperation.execute(client) to createFolderOperation
186250 }
187251
188252 @Suppress(" DEPRECATION" )
189- private suspend fun createFile (operation : OfflineOperationEntity , client : OwnCloudClient ): OfflineOperationResult {
253+ private fun createFile (operation : OfflineOperationEntity , client : OwnCloudClient ): OfflineOperationResult {
190254 val operationType = (operation.type as OfflineOperationType .CreateFile )
191-
192- val createFileOperation = withContext(NonCancellable ) {
193- val lastModificationDate = System .currentTimeMillis() / ONE_SECOND
194-
195- UploadFileRemoteOperation (
196- operationType.localPath,
197- operationType.remotePath,
198- operationType.mimeType,
199- " " ,
200- operation.modifiedAt ? : lastModificationDate,
201- operation.createdAt ? : System .currentTimeMillis(),
202- true
203- )
204- }
205-
255+ val lastModificationDate = System .currentTimeMillis() / ONE_SECOND
256+ val createFileOperation = UploadFileRemoteOperation (
257+ operationType.localPath,
258+ operationType.remotePath,
259+ operationType.mimeType,
260+ " " ,
261+ operation.modifiedAt ? : lastModificationDate,
262+ operation.createdAt ? : System .currentTimeMillis(),
263+ true
264+ )
206265 return createFileOperation.execute(client) to createFileOperation
207266 }
208267
209268 @Suppress(" DEPRECATION" )
210- private suspend fun renameFile (operation : OfflineOperationEntity , client : OwnCloudClient ): OfflineOperationResult {
211- val renameFileOperation = withContext(NonCancellable ) {
212- val operationType = (operation.type as OfflineOperationType .RenameFile )
213- RenameFileOperation (operation.path, operationType.newName, fileDataStorageManager)
214- }
215-
269+ private fun renameFile (operation : OfflineOperationEntity , client : OwnCloudClient ): OfflineOperationResult {
270+ val operationType = (operation.type as OfflineOperationType .RenameFile )
271+ val renameFileOperation = RenameFileOperation (operation.path, operationType.newName, fileDataStorageManager)
216272 return renameFileOperation.execute(client) to renameFileOperation
217273 }
218274
219275 @Suppress(" DEPRECATION" )
220- private suspend fun removeFile (ocFile : OCFile , client : OwnCloudClient ): OfflineOperationResult {
221- val removeFileOperation = withContext(NonCancellable ) {
222- RemoveFileOperation (ocFile, false , user, true , context, fileDataStorageManager)
223- }
224-
276+ private fun removeFile (ocFile : OCFile , client : OwnCloudClient ): OfflineOperationResult {
277+ val removeFileOperation = RemoveFileOperation (ocFile, false , user, true , context, fileDataStorageManager)
225278 return removeFileOperation.execute(client) to removeFileOperation
226279 }
227280 // endregion
228281
229- private suspend fun handleResult (
230- operation : OfflineOperationEntity ,
231- totalOperations : Int ,
232- currentSuccessfulOperationIndex : Int ,
233- result : OfflineOperationResult
234- ): Boolean {
235- val operationResult = result?.first ? : return false
236-
237- val logMessage = if (operationResult.isSuccess) " Operation completed" else " Operation failed"
238- Log_OC .d(TAG , " $logMessage filename: ${operation.filename} , type: ${operation.type} " )
239-
240- return if (result.first?.isSuccess == true ) {
241- handleSuccessResult(operation, totalOperations, currentSuccessfulOperationIndex)
242- true
243- } else {
244- handleErrorResult(operation.id, result)
245- false
246- }
247- }
248-
249- private suspend fun handleSuccessResult (
250- operation : OfflineOperationEntity ,
251- totalOperations : Int ,
252- currentSuccessfulOperationIndex : Int
253- ) {
254- if (operation.type is OfflineOperationType .RemoveFile ) {
255- val operationType = operation.type as OfflineOperationType .RemoveFile
256- fileDataStorageManager.getFileByDecryptedRemotePath(operationType.path)?.let { ocFile ->
257- repository.deleteOperation(ocFile)
258- }
259- } else {
260- repository.updateNextOperations(operation)
261- }
262-
263- fileDataStorageManager.offlineOperationDao.delete(operation)
264-
265- notificationManager.update(totalOperations, currentSuccessfulOperationIndex, operation.filename ? : " " )
266- delay(ONE_SECOND )
267- notificationManager.dismissNotification(operation.id)
268- }
269-
270- private fun handleErrorResult (id : Int? , result : OfflineOperationResult ) {
271- val operationResult = result?.first ? : return
272- val operation = result.second ? : return
273- Log_OC .e(TAG , " ❌ Operation failed [id=$id ]: code=${operationResult.code} , message=${operationResult.message} " )
274- val excludedErrorCodes = listOf (RemoteOperationResult .ResultCode .FOLDER_ALREADY_EXISTS )
275-
276- if (! excludedErrorCodes.contains(operationResult.code)) {
277- notificationManager.showNewNotification(id, operationResult, operation)
278- } else {
279- Log_OC .d(TAG , " ℹ️ Ignored error: ${operationResult.code} " )
280- }
281- }
282-
283282 @Suppress(" DEPRECATION" )
284283 private fun getRemoteFile (remotePath : String ): RemoteFile ? {
285284 val mimeType = MimeTypeUtil .getMimeTypeFromPath(remotePath)
0 commit comments