Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;

import com.owncloud.android.MainApp;
Expand All @@ -30,6 +31,9 @@ public interface UserAccountManager extends CurrentAccountProvider {
String ACCOUNT_USES_STANDARD_PASSWORD = "ACCOUNT_USES_STANDARD_PASSWORD";
String PENDING_FOR_REMOVAL = "PENDING_FOR_REMOVAL";

@Nullable
Context getContext();

@Nullable
OwnCloudAccount getCurrentOwnCloudAccount();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public class UserAccountManagerImpl implements UserAccountManager {
private static final String TAG = UserAccountManagerImpl.class.getSimpleName();
private static final String PREF_SELECT_OC_ACCOUNT = "select_oc_account";

private Context context;
private final Context context;
private final AccountManager accountManager;

public static UserAccountManagerImpl fromContext(Context context) {
Expand Down Expand Up @@ -305,6 +305,12 @@ public User getAnonymousUser() {
return AnonymousUser.fromContext(context);
}

@Nullable
@Override
public Context getContext() {
return context;
}

@Override
@Nullable
public OwnCloudAccount getCurrentOwnCloudAccount() {
Expand Down
27 changes: 27 additions & 0 deletions app/src/main/java/com/nextcloud/client/di/DispatcherModule.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* 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
Expand All @@ -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
Expand All @@ -38,4 +51,18 @@ 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)
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ 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
import com.nextcloud.client.notifications.AppWideNotificationManager
import com.nextcloud.utils.extensions.checkWCFRestrictions
import com.nextcloud.utils.extensions.createOwncloudClient
import com.nextcloud.utils.extensions.getUploadIds
import com.nextcloud.utils.extensions.isAnonymous
import com.nextcloud.utils.extensions.isLastResultConflictError
import com.nextcloud.utils.extensions.isSame
import com.owncloud.android.MainApp
Expand All @@ -38,7 +39,6 @@ import com.owncloud.android.db.OCUpload
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.network.OnDatatransferProgressListener
import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.common.utils.Log_OC
Expand All @@ -56,7 +56,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")
Expand All @@ -74,26 +74,28 @@ class FileUploadHelper {
@Inject
lateinit var fileStorageManager: FileDataStorageManager

private val ioScope = CoroutineScope(Dispatchers.IO)
@Inject
@ApplicationScope
lateinit var appScope: CoroutineScope

init {
MainApp.getAppComponent().inject(this)
}

private val uploadActionHandler = UploadListAdapterActionHandler()

companion object {
private val TAG = FileUploadWorker::class.java.simpleName

const val MAX_FILE_COUNT = 500

val mBoundListeners = HashMap<String, OnDatatransferProgressListener>()

private var instance: FileUploadHelper? = null
private val retryInProgress = AtomicBoolean(false)

private val retryFailedUploadsSemaphore = Semaphore(1)
private val sharedInstance: FileUploadHelper by lazy { FileUploadHelper() }

fun instance(): FileUploadHelper = instance ?: synchronized(this) {
instance ?: FileUploadHelper().also { instance = it }
}
fun instance(): FileUploadHelper = sharedInstance

fun buildRemoteName(accountName: String, remotePath: String): String = accountName + remotePath
}
Expand Down Expand Up @@ -122,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)

try {
ioScope.launch {
appScope.launch {
try {
val uploads = getUploadsByStatus(null, UploadStatus.UPLOAD_FAILED, capability)
if (uploads.isNotEmpty()) {
isUploadStarted = true
}

retryUploads(
uploadsStorageManager,
Expand All @@ -145,12 +143,12 @@ class FileUploadHelper {
powerManagementService,
uploads
)
} finally {
// 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.
retryInProgress.set(false)
}
} finally {
retryFailedUploadsSemaphore.release()
}

return isUploadStarted
}

suspend fun retryCancelledUploads(
Expand Down Expand Up @@ -185,21 +183,13 @@ class FileUploadHelper {
val batteryStatus = powerManagementService.battery

val uploadsToRetry = mutableListOf<Long>()

val currentAccount = accountManager.currentAccount
val context = MainApp.getAppContext()
var ownCloudClient: OwnCloudClient? = null
if (!currentAccount.isAnonymous(context)) {
ownCloudClient =
OwnCloudClientFactory.createOwnCloudClient(accountManager.currentAccount, MainApp.getAppContext())
}
val uploadActionHandler = UploadListAdapterActionHandler()
val client = accountManager.createOwncloudClient()

for (upload in uploads) {
if (upload.isLastResultConflictError()) {
ownCloudClient?.let {
client?.let {
conflictHandlingResult =
uploadActionHandler.handleConflict(upload, ownCloudClient, uploadsStorageManager)
uploadActionHandler.handleConflict(upload, client = it, uploadsStorageManager)
}
continue
}
Expand All @@ -214,7 +204,7 @@ class FileUploadHelper {

if (uploadResult != UploadResult.UPLOADED) {
if (upload.lastResult != uploadResult) {
// Setting Upload status else cancelled uploads will behave wrong, when retrying
// Setting Upload status else canceled uploads will behave wrong, when retrying
// Needs to happen first since lastResult wil be overwritten by setter
upload.uploadStatus = UploadStatus.UPLOAD_FAILED

Expand Down Expand Up @@ -345,7 +335,7 @@ class FileUploadHelper {
status: UploadStatus,
onCompleted: () -> Unit = {}
) {
ioScope.launch {
appScope.launch {
uploadsStorageManager.uploadDao.updateStatus(remotePath, accountName, status.value)
onCompleted()
}
Expand Down Expand Up @@ -531,7 +521,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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils.extensions

import com.nextcloud.client.account.UserAccountManager
import com.owncloud.android.MainApp
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.utils.Log_OC

private const val TAG = "UserAccountManagerExtensions"

fun UserAccountManager.createOwncloudClient(): OwnCloudClient? = createOwncloudClient(currentAccount.name)

@Suppress("TooGenericExceptionCaught", "ReturnCount", "DEPRECATION")
fun UserAccountManager.createOwncloudClient(accountName: String): OwnCloudClient? {
val context = context ?: MainApp.getAppContext()
if (context == null) {
Log_OC.e(TAG, "app context is null, cannot create client")
return null
}

val user = getUser(accountName).orElse(null)
if (user == null || user.isAnonymous) {
Log_OC.e(TAG, "account is not registered, cannot create client for: $accountName")
return null
}

return try {
val result = OwnCloudClientFactory.createOwnCloudClient(user.toPlatformAccount(), context)
Log_OC.i(TAG, "client created")
result
} catch (e: AccountUtils.AccountNotFoundException) {
Log_OC.e(TAG, "account removed while creating client for: $accountName", e)
null
} catch (e: Exception) {
Log_OC.e(TAG, "cannot create client: ", e)
null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading
Loading