From 744136838f142a9a07ced8280e057b1f656146cc Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 24 Jul 2026 11:00:43 +0200 Subject: [PATCH 1/4] fix crash Signed-off-by: alperozturk96 --- .../client/jobs/upload/FileUploadHelper.kt | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index 816c81898b24..a5c1e1457847 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -39,6 +39,7 @@ import com.owncloud.android.db.UploadResult import com.owncloud.android.files.services.NameCollisionPolicy import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.OwnCloudClientFactory +import com.owncloud.android.lib.common.accounts.AccountUtils import com.owncloud.android.lib.common.network.OnDatatransferProgressListener import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC @@ -51,8 +52,10 @@ import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.ui.adapter.uploadList.helper.ConflictHandlingResult import com.owncloud.android.ui.adapter.uploadList.helper.UploadListAdapterActionHandler import com.owncloud.android.utils.DisplayUtils +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File @@ -74,7 +77,13 @@ class FileUploadHelper { @Inject lateinit var fileStorageManager: FileDataStorageManager - private val ioScope = CoroutineScope(Dispatchers.IO) + private val ioScope = CoroutineScope( + SupervisorJob() + + Dispatchers.IO + + CoroutineExceptionHandler { _, throwable -> + Log_OC.e(TAG, "Uncaught exception in FileUploadHelper coroutine", throwable) + } + ) init { MainApp.getAppComponent().inject(this) @@ -131,8 +140,8 @@ class FileUploadHelper { var isUploadStarted = false val capability = fileStorageManager.getCapability(accountManager.user) - try { - ioScope.launch { + ioScope.launch { + try { val uploads = getUploadsByStatus(null, UploadStatus.UPLOAD_FAILED, capability) if (uploads.isNotEmpty()) { isUploadStarted = true @@ -145,9 +154,11 @@ class FileUploadHelper { powerManagementService, uploads ) + } finally { + // Release only after retry processing has completely finished so the semaphore guards + // coroutine execution, not just its launch. This keeps a single retry running at a time. + retryFailedUploadsSemaphore.release() } - } finally { - retryFailedUploadsSemaphore.release() } return isUploadStarted @@ -190,8 +201,12 @@ class FileUploadHelper { val context = MainApp.getAppContext() var ownCloudClient: OwnCloudClient? = null if (!currentAccount.isAnonymous(context)) { - ownCloudClient = + ownCloudClient = try { OwnCloudClientFactory.createOwnCloudClient(accountManager.currentAccount, MainApp.getAppContext()) + } catch (e: AccountUtils.AccountNotFoundException) { + Log_OC.e(TAG, "Cannot create client, account not found; skipping conflict handling", e) + null + } } val uploadActionHandler = UploadListAdapterActionHandler() From bfe677f2a9a2bb7a27b90a84caf7d53513ccc22e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 24 Jul 2026 11:09:31 +0200 Subject: [PATCH 2/4] introduce application scope to DispatcherModule.kt Signed-off-by: alperozturk96 --- .../nextcloud/client/di/DispatcherModule.kt | 28 +++++++++++++++++++ .../client/jobs/upload/FileUploadHelper.kt | 19 +++++-------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/di/DispatcherModule.kt b/app/src/main/java/com/nextcloud/client/di/DispatcherModule.kt index ab15cdf6e541..18fe0aadda65 100644 --- a/app/src/main/java/com/nextcloud/client/di/DispatcherModule.kt +++ b/app/src/main/java/com/nextcloud/client/di/DispatcherModule.kt @@ -1,17 +1,23 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di +import com.owncloud.android.lib.common.utils.Log_OC import dagger.Module import dagger.Provides import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import javax.inject.Qualifier +import javax.inject.Singleton @Retention(AnnotationRetention.BINARY) @Qualifier @@ -25,8 +31,15 @@ annotation class IoDispatcher @Qualifier annotation class MainDispatcher +@Retention(AnnotationRetention.BINARY) +@Qualifier +annotation class ApplicationScope + @Module object DispatcherModule { + + private const val APPLICATION_SCOPE_TAG = "ApplicationScope" + @DefaultDispatcher @Provides fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default @@ -38,4 +51,19 @@ object DispatcherModule { @MainDispatcher @Provides fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main + + /** + * A process-lifetime [CoroutineScope] for singletons that outlive any single Android component. + */ + @ApplicationScope + @Provides + @Singleton + fun provideApplicationScope(@IoDispatcher dispatcher: CoroutineDispatcher): CoroutineScope = + CoroutineScope( + SupervisorJob() + + dispatcher + + CoroutineExceptionHandler { _, throwable -> + Log_OC.e(APPLICATION_SCOPE_TAG, "Uncaught exception in application coroutine scope", throwable) + } + ) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index a5c1e1457847..6978d7102387 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -19,6 +19,7 @@ import com.nextcloud.client.database.entity.toOCUpload import com.nextcloud.client.database.entity.toUploadEntity import com.nextcloud.client.device.BatteryStatus import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.client.di.ApplicationScope import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.network.Connectivity import com.nextcloud.client.network.ConnectivityService @@ -52,10 +53,8 @@ import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.ui.adapter.uploadList.helper.ConflictHandlingResult import com.owncloud.android.ui.adapter.uploadList.helper.UploadListAdapterActionHandler import com.owncloud.android.utils.DisplayUtils -import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File @@ -77,13 +76,9 @@ class FileUploadHelper { @Inject lateinit var fileStorageManager: FileDataStorageManager - private val ioScope = CoroutineScope( - SupervisorJob() + - Dispatchers.IO + - CoroutineExceptionHandler { _, throwable -> - Log_OC.e(TAG, "Uncaught exception in FileUploadHelper coroutine", throwable) - } - ) + @Inject + @ApplicationScope + lateinit var appScope: CoroutineScope init { MainApp.getAppComponent().inject(this) @@ -140,7 +135,7 @@ class FileUploadHelper { var isUploadStarted = false val capability = fileStorageManager.getCapability(accountManager.user) - ioScope.launch { + appScope.launch { try { val uploads = getUploadsByStatus(null, UploadStatus.UPLOAD_FAILED, capability) if (uploads.isNotEmpty()) { @@ -360,7 +355,7 @@ class FileUploadHelper { status: UploadStatus, onCompleted: () -> Unit = {} ) { - ioScope.launch { + appScope.launch { uploadsStorageManager.uploadDao.updateStatus(remotePath, accountName, status.value) onCompleted() } @@ -546,7 +541,7 @@ class FileUploadHelper { * @param user Needed for creating client */ fun removeDuplicatedFile(duplicatedFile: OCFile, client: OwnCloudClient, user: User, onCompleted: () -> Unit) { - ioScope.launch { + appScope.launch { val removeFileOperation = RemoveFileOperation( duplicatedFile, false, From 2d143e65f9acba01407977815ade82a6f879da32 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 24 Jul 2026 11:14:03 +0200 Subject: [PATCH 3/4] use lazy for shared instance Signed-off-by: alperozturk96 --- .../com/nextcloud/client/jobs/upload/FileUploadHelper.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index 6978d7102387..50492794ce3e 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -91,13 +91,11 @@ class FileUploadHelper { val mBoundListeners = HashMap() - private var instance: FileUploadHelper? = null - private val retryFailedUploadsSemaphore = Semaphore(1) - fun instance(): FileUploadHelper = instance ?: synchronized(this) { - instance ?: FileUploadHelper().also { instance = it } - } + private val sharedInstance: FileUploadHelper by lazy { FileUploadHelper() } + + fun instance(): FileUploadHelper = sharedInstance fun buildRemoteName(accountName: String, remotePath: String): String = accountName + remotePath } From f01298f72fb578943ce581012cc3ded8a6e5a3fd Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 24 Jul 2026 11:23:58 +0200 Subject: [PATCH 4/4] use atomic boolean instead of semaphore and remove broken flag since events will be firing Signed-off-by: alperozturk96 --- .../client/jobs/upload/FileUploadHelper.kt | 20 +++++++------------ .../android/ui/activity/UploadListActivity.kt | 6 +----- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index 50492794ce3e..c065b02f3ca6 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -58,7 +58,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File -import java.util.concurrent.Semaphore +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @Suppress("TooManyFunctions") @@ -91,7 +91,7 @@ class FileUploadHelper { val mBoundListeners = HashMap() - private val retryFailedUploadsSemaphore = Semaphore(1) + private val retryInProgress = AtomicBoolean(false) private val sharedInstance: FileUploadHelper by lazy { FileUploadHelper() } @@ -124,21 +124,17 @@ class FileUploadHelper { connectivityService: ConnectivityService, accountManager: UserAccountManager, powerManagementService: PowerManagementService - ): Boolean { - if (!retryFailedUploadsSemaphore.tryAcquire()) { + ) { + if (!retryInProgress.compareAndSet(false, true)) { Log_OC.d(TAG, "skipping retryFailedUploads, already running") - return true + return } - var isUploadStarted = false val capability = fileStorageManager.getCapability(accountManager.user) appScope.launch { try { val uploads = getUploadsByStatus(null, UploadStatus.UPLOAD_FAILED, capability) - if (uploads.isNotEmpty()) { - isUploadStarted = true - } retryUploads( uploadsStorageManager, @@ -148,13 +144,11 @@ class FileUploadHelper { uploads ) } finally { - // Release only after retry processing has completely finished so the semaphore guards + // Reset only after retry processing has completely finished so the guard covers // coroutine execution, not just its launch. This keeps a single retry running at a time. - retryFailedUploadsSemaphore.release() + retryInProgress.set(false) } } - - return isUploadStarted } suspend fun retryCancelledUploads( diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt index fc742f2ac97f..b6ef36356a6b 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt @@ -168,16 +168,12 @@ class UploadListActivity : } private fun refresh() { - val isUploadStarted = FileUploadHelper.instance().retryFailedUploads( + FileUploadHelper.instance().retryFailedUploads( uploadsStorageManager, connectivityService, accountManager, powerManagementService ) - - if (!isUploadStarted) { - uploadListAdapter.loadUploadItemsFromDb { swipeListRefreshLayout?.isRefreshing = false } - } } override fun onStart() {