From 9c61e3c1691c7062593f8abc438488f61447ee64 Mon Sep 17 00:00:00 2001 From: zerox80 Date: Wed, 3 Dec 2025 21:47:08 +0100 Subject: [PATCH 1/3] feat: add cancellation support for TUS and standard file uploads --- .../opencloud/android/workers/TusUploadHelper.kt | 14 +++++++++++++- .../workers/UploadFileFromContentUriWorker.kt | 13 +++++++++++++ .../workers/UploadFileFromFileSystemWorker.kt | 13 +++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt index cce93c875..32ec7d85b 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt @@ -21,6 +21,13 @@ import kotlin.math.min class TusUploadHelper( private val transferRepository: TransferRepository, ) { + @Volatile + private var cancelled = false + + fun cancel() { + cancelled = true + Timber.d("TUS: upload cancellation requested") + } /** * Runs the full TUS upload flow. On success the method returns normally. On failure an exception @@ -167,11 +174,16 @@ class TusUploadHelper( val httpOverride = tusSupport?.httpMethodOverride var consecutiveFailures = 0 - while (offset < fileSize) { + while (offset < fileSize && !cancelled) { val remaining = fileSize - offset val chunkSize = min(DEFAULT_CHUNK_SIZE, min(remaining, serverMaxChunk ?: Long.MAX_VALUE)) Timber.d("TUS: uploading chunk=%d at offset=%d remaining=%d", chunkSize, offset, remaining) + if (cancelled) { + Timber.i("TUS: upload cancelled by user at offset %d", offset) + throw java.io.InterruptedIOException("TUS upload cancelled by user") + } + val patchOperation = PatchTusUploadChunkRemoteOperation( localPath = localPath, uploadUrl = resolvedTusUrl, diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt index 0e3ce381b..4025d811f 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt @@ -361,6 +361,11 @@ class UploadFileFromContentUriWorker( } private fun updateProgressFromTus(offset: Long, totalSize: Long) { + if (this.isStopped) { + Timber.w("Cancelling TUS upload. The worker is stopped by user or system") + tusUploadHelper.cancel() + } + if (totalSize <= 0) return val percent: Int = (100.0 * offset.toDouble() / totalSize.toDouble()).toInt() if (percent == lastPercent) return @@ -453,6 +458,14 @@ class UploadFileFromContentUriWorker( totalToTransfer: Long, filePath: String ) { + if (this.isStopped) { + Timber.w("Cancelling upload operation. The worker is stopped by user or system") + if (::uploadFileOperation.isInitialized) { + uploadFileOperation.cancel() + uploadFileOperation.removeDataTransferProgressListener(this) + } + } + val percent: Int = (100.0 * totalTransferredSoFar.toDouble() / totalToTransfer.toDouble()).toInt() if (percent == lastPercent) return diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt index 1fcf54072..d86bf1bfc 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt @@ -338,6 +338,11 @@ class UploadFileFromFileSystemWorker( } private fun updateProgressFromTus(offset: Long, totalSize: Long) { + if (this.isStopped) { + Timber.w("Cancelling TUS upload. The worker is stopped by user or system") + tusUploadHelper.cancel() + } + if (totalSize <= 0) return val percent: Int = (100.0 * offset.toDouble() / totalSize.toDouble()).toInt() if (percent == lastPercent) return @@ -458,6 +463,14 @@ class UploadFileFromFileSystemWorker( totalToTransfer: Long, filePath: String ) { + if (this.isStopped) { + Timber.w("Cancelling upload operation. The worker is stopped by user or system") + if (::uploadFileOperation.isInitialized) { + uploadFileOperation.cancel() + uploadFileOperation.removeDataTransferProgressListener(this) + } + } + val percent: Int = (100.0 * totalTransferredSoFar.toDouble() / totalToTransfer.toDouble()).toInt() if (percent == lastPercent) return From 76d5b81aa259ffe484b5e8989e53b9af40c2aed9 Mon Sep 17 00:00:00 2001 From: zerox80 Date: Wed, 3 Dec 2025 16:56:58 +0100 Subject: [PATCH 2/3] TUS: Fix #71 --- .../android/workers/TusUploadHelper.kt | 28 +++++++++++++++++-- .../workers/UploadFileFromFileSystemWorker.kt | 1 + 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt index 32ec7d85b..6db2857da 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt @@ -11,6 +11,7 @@ import eu.opencloud.android.lib.resources.files.chunks.ChunkedUploadFromFileSyst import eu.opencloud.android.lib.resources.files.tus.CreateTusUploadRemoteOperation import eu.opencloud.android.lib.resources.files.tus.GetTusUploadOffsetRemoteOperation import eu.opencloud.android.lib.resources.files.tus.PatchTusUploadChunkRemoteOperation +import eu.opencloud.android.domain.exceptions.FileNotFoundException import timber.log.Timber import java.io.File import kotlin.math.min @@ -23,10 +24,12 @@ class TusUploadHelper( ) { @Volatile private var cancelled = false + private var activePatchOperation: PatchTusUploadChunkRemoteOperation? = null fun cancel() { cancelled = true Timber.d("TUS: upload cancellation requested") + activePatchOperation?.cancel() } /** @@ -48,6 +51,8 @@ class TusUploadHelper( progressCallback: ((Long, Long) -> Unit)? = null, spaceWebDavUrl: String? = null, ) { + // Reset cancelled state for new upload + cancelled = false Timber.d("TUS: starting upload for %s size=%d", remotePath, fileSize) var tusUrl = transfer.tusUploadUrl @@ -138,7 +143,8 @@ class TusUploadHelper( tusSupport = tusSupport, progressListener = progressListener, progressCallback = progressCallback, - initialOffset = offset + initialOffset = offset, + uploadId = uploadId, ) // Verify upload is actually complete @@ -167,7 +173,8 @@ class TusUploadHelper( tusSupport: OCCapability.TusSupport?, progressListener: OnDatatransferProgressListener?, progressCallback: ((Long, Long) -> Unit)?, - initialOffset: Long + initialOffset: Long, + uploadId: Long, ): Long { var offset = initialOffset val serverMaxChunk = tusSupport?.maxChunkSize?.takeIf { it > 0 }?.toLong() @@ -193,8 +200,10 @@ class TusUploadHelper( ).apply { progressListener?.let { addDataTransferProgressListener(it) } } + activePatchOperation = patchOperation val patchResult = patchOperation.execute(client) + activePatchOperation = null if (!patchResult.isSuccess || patchResult.data == null || patchResult.data!! < offset) { consecutiveFailures++ Timber.w( @@ -211,6 +220,7 @@ class TusUploadHelper( currentOffset = offset, totalSize = fileSize, progressCallback = progressCallback, + uploadId = uploadId, ) if (recoveredOffset != null && recoveredOffset > offset) { @@ -297,6 +307,7 @@ class TusUploadHelper( currentOffset: Long, totalSize: Long, progressCallback: ((Long, Long) -> Unit)?, + uploadId: Long, ): Long? = try { val newOffset = executeRemoteOperation { GetTusUploadOffsetRemoteOperation(tusUrl).execute(client) @@ -321,6 +332,19 @@ class TusUploadHelper( } catch (e: java.io.IOException) { Timber.w(e, "TUS: recover offset failed") throw e + } catch (e: FileNotFoundException) { + Timber.w(e, "TUS: upload not found on server (404), clearing state to restart") + transferRepository.updateTusState( + id = uploadId, + tusUploadUrl = null, + tusUploadLength = null, + tusUploadMetadata = null, + tusUploadChecksum = null, + tusResumableVersion = null, + tusUploadExpires = null, + tusUploadConcat = null, + ) + throw java.io.IOException("TUS: upload session lost (404), forcing restart", e) } catch (recoverError: Throwable) { Timber.w(recoverError, "TUS: recover offset failed") null diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt index d86bf1bfc..218611fd6 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt @@ -465,6 +465,7 @@ class UploadFileFromFileSystemWorker( ) { if (this.isStopped) { Timber.w("Cancelling upload operation. The worker is stopped by user or system") + tusUploadHelper.cancel() if (::uploadFileOperation.isInitialized) { uploadFileOperation.cancel() uploadFileOperation.removeDataTransferProgressListener(this) From 0318e3392c5594f6b2cba343b6a85d652e8016e1 Mon Sep 17 00:00:00 2001 From: zerox80 Date: Tue, 25 Nov 2025 10:03:25 +0100 Subject: [PATCH 3/3] chore: detekt - collapse nested if in LoginActivity --- .../android/presentation/authentication/LoginActivity.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/LoginActivity.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/LoginActivity.kt index e926993f2..57ded5f90 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/LoginActivity.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/LoginActivity.kt @@ -130,11 +130,9 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted userAccount = intent.getParcelableExtra(EXTRA_ACCOUNT) // Get values from savedInstanceState - if (savedInstanceState == null) { - if (authTokenType == null && userAccount != null) { - authenticationViewModel.supportsOAuth2((userAccount as Account).name) - } - } else { + if (savedInstanceState == null && authTokenType == null && userAccount != null) { + authenticationViewModel.supportsOAuth2((userAccount as Account).name) + } else if (savedInstanceState != null) { authTokenType = savedInstanceState.getString(KEY_AUTH_TOKEN_TYPE) }