Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 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,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)
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,6 +40,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
Expand All @@ -56,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")
Expand All @@ -74,7 +76,9 @@ class FileUploadHelper {
@Inject
lateinit var fileStorageManager: FileDataStorageManager

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

init {
MainApp.getAppComponent().inject(this)
Expand All @@ -87,13 +91,11 @@ class FileUploadHelper {

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 @@ -190,8 +188,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()

Expand Down Expand Up @@ -345,7 +347,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 +533,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
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