From 1e8b77d68bf9f8f69cc27e1da850bf6c9a0387f7 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 28 May 2026 09:31:29 +0300 Subject: [PATCH 1/4] Rename .java to .kt Signed-off-by: alperozturk96 --- .../{DownloadFileOperation.java => DownloadFileOperation.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/owncloud/android/operations/{DownloadFileOperation.java => DownloadFileOperation.kt} (100%) diff --git a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java rename to app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt From 713cd498e477bb157960aff99dffcb0c44fe1e73 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 28 May 2026 09:31:31 +0300 Subject: [PATCH 2/4] wip Signed-off-by: alperozturk96 --- .../java/com/owncloud/android/DownloadIT.kt | 11 +- .../client/jobs/download/DownloadTask.kt | 4 +- .../jobs/download/FileDownloadHelper.kt | 4 +- .../jobs/download/FileDownloadWorker.kt | 7 +- .../operations/DownloadFileOperation.kt | 490 +++++++----------- 5 files changed, 206 insertions(+), 310 deletions(-) diff --git a/app/src/androidTest/java/com/owncloud/android/DownloadIT.kt b/app/src/androidTest/java/com/owncloud/android/DownloadIT.kt index e5c3591597c4..7298142d89df 100644 --- a/app/src/androidTest/java/com/owncloud/android/DownloadIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/DownloadIT.kt @@ -17,6 +17,7 @@ import com.owncloud.android.operations.RemoveFileOperation import com.owncloud.android.utils.FileStorageUtils import org.junit.After import org.junit.Assert +import org.junit.Assert.fail import org.junit.Test import kotlin.io.path.Path import kotlin.io.path.exists @@ -72,11 +73,17 @@ class DownloadIT : AbstractOnServerIT() { var file1 = fileDataStorageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty.txt") var file2 = fileDataStorageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty2.txt") - val operation1 = DownloadFileOperation(user, file1, targetContext) + if (file1 == null) { + fail("file 1 cannot be null") + } + val operation1 = DownloadFileOperation(user, file1!!, targetContext) val operation1Result = operation1.execute(client) Assert.assertTrue(operation1Result.isSuccess) - val operation2 = DownloadFileOperation(user, file2, targetContext) + if (file2 == null) { + fail("file 2 cannot be null") + } + val operation2 = DownloadFileOperation(user, file2!!, targetContext) val operation2Result = operation2.execute(client) Assert.assertTrue(operation2Result.isSuccess) diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/DownloadTask.kt b/app/src/main/java/com/nextcloud/client/jobs/download/DownloadTask.kt index b8708dfd5b26..2da0e9eec5b6 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/DownloadTask.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/DownloadTask.kt @@ -74,8 +74,8 @@ class DownloadTask( lastSyncDateForProperties = syncDate lastSyncDateForData = syncDate isUpdateThumbnailNeeded = true - modificationTimestamp = op.modificationTimestamp - modificationTimestampAtLastSyncForData = op.modificationTimestamp + modificationTimestamp = op.getModificationTimestamp() + modificationTimestampAtLastSyncForData = op.getModificationTimestamp() etag = op.etag mimeType = op.mimeType storagePath = op.savePath diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt index 240fda49ef75..f9bbcd71ce5a 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt @@ -79,8 +79,8 @@ class FileDownloadHelper { lastSyncDateForProperties = syncDate lastSyncDateForData = syncDate isUpdateThumbnailNeeded = true - modificationTimestamp = currentDownload?.modificationTimestamp ?: 0L - modificationTimestampAtLastSyncForData = currentDownload?.modificationTimestamp ?: 0L + modificationTimestamp = currentDownload?.getModificationTimestamp() ?: 0L + modificationTimestampAtLastSyncForData = currentDownload?.getModificationTimestamp() ?: 0L etag = currentDownload?.etag mimeType = currentDownload?.mimeType storagePath = currentDownload?.savePath diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt index d9febd6e48b3..0a4657a9fd56 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt @@ -175,6 +175,11 @@ class FileDownloadWorker( val requestedDownloads: AbstractList = Vector() + val user = user ?: run { + Log_OC.e(TAG, "user cannot be null") + return requestedDownloads + } + return try { files.forEach { file -> val operation = DownloadFileOperation( @@ -190,7 +195,7 @@ class FileDownloadWorker( operation.addDownloadDataTransferProgressListener(this) operation.addDownloadDataTransferProgressListener(downloadProgressListener) val (downloadKey, _) = pendingDownloads.putIfAbsent( - user?.accountName, + user.accountName, file.remotePath, operation ) ?: Pair(null, null) diff --git a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt index 84ed094efda2..217a464d2d7a 100644 --- a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt @@ -9,351 +9,235 @@ * SPDX-FileCopyrightText: 2012 David A. Velasco * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ -package com.owncloud.android.operations; - -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.text.TextUtils; -import android.webkit.MimeTypeMap; - -import com.nextcloud.client.account.User; -import com.nextcloud.common.NextcloudClient; -import com.nextcloud.utils.extensions.ContextExtensionsKt; -import com.nextcloud.utils.extensions.OwnCloudClientExtensionsKt; -import com.owncloud.android.R; -import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1; -import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile; -import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.network.OnDatatransferProgressListener; -import com.owncloud.android.lib.common.operations.OperationCancelledException; -import com.owncloud.android.lib.common.operations.RemoteOperation; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.files.DownloadFileRemoteOperation; -import com.owncloud.android.utils.EncryptionUtils; -import com.owncloud.android.utils.FileExportUtils; -import com.owncloud.android.utils.FileStorageUtils; - -import java.io.File; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.nio.file.Files; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.crypto.Cipher; - -import static com.owncloud.android.utils.EncryptionUtils.decodeStringToBase64Bytes; - -/** - * Remote DownloadOperation performing the download of a file to an ownCloud server - */ -public class DownloadFileOperation extends RemoteOperation { - private static final String TAG = DownloadFileOperation.class.getSimpleName(); - - private User user; - private OCFile file; - private String behaviour; - private String etag = ""; - private String activityName; - private String packageName; - private DownloadType downloadType; - - private final WeakReference context; - - // CHECK: Is this still needed after conversion from Foreground Services to Worker? - private Set dataTransferListeners = new HashSet<>(); - - private long modificationTimestamp; - private DownloadFileRemoteOperation downloadOperation; - private final AtomicBoolean cancellationRequested = new AtomicBoolean(false); - private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); - - public DownloadFileOperation(User user, - OCFile file, - String behaviour, - String activityName, - String packageName, - Context context, - DownloadType downloadType) { - if (user == null) { - throw new IllegalArgumentException("Illegal null user in DownloadFileOperation " + - "creation"); - } - if (file == null) { - throw new IllegalArgumentException("Illegal null file in DownloadFileOperation " + - "creation"); - } - - this.user = user; - this.file = file; - this.behaviour = behaviour; - this.activityName = activityName; - this.packageName = packageName; - this.context = new WeakReference<>(context); - this.downloadType = downloadType; - } - - public DownloadFileOperation(User user, OCFile file, Context context) { - this(user, file, null, null, null, context, DownloadType.DOWNLOAD); - } - - public boolean isMatching(String accountName, long fileId) { - return getFile().getFileId() == fileId && getUser().getAccountName().equals(accountName); - } - - public void cancelMatchingOperation(String accountName, long fileId) { - if (isMatching(accountName, fileId)) { - cancel(); - } - } - - public String getSavePath() { - if (file.getStoragePath() != null) { - File parentFile = new File(file.getStoragePath()).getParentFile(); - if (parentFile != null && !parentFile.exists()) { - try { - Files.createDirectories(parentFile.toPath()); - } catch (IOException e) { - return FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), file); +package com.owncloud.android.operations + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.webkit.MimeTypeMap +import com.nextcloud.client.account.User +import com.nextcloud.utils.extensions.showToast +import com.nextcloud.utils.extensions.toNextcloudClient +import com.owncloud.android.R +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1 +import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.network.OnDatatransferProgressListener +import com.owncloud.android.lib.common.operations.OperationCancelledException +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.DownloadFileRemoteOperation +import com.owncloud.android.utils.EncryptionUtils +import com.owncloud.android.utils.FileExportUtils +import com.owncloud.android.utils.FileStorageUtils +import java.io.File +import java.lang.ref.WeakReference +import java.nio.file.Files +import java.util.concurrent.atomic.AtomicBoolean +import javax.crypto.Cipher + +class DownloadFileOperation( + val user: User, + val file: OCFile, + val behaviour: String?, + val activityName: String?, + val packageName: String?, + context: Context?, + var downloadType: DownloadType? +) : RemoteOperation() { + + var etag: String? = "" + private set + + private val context = WeakReference(context) + private val dataTransferListeners: MutableSet = HashSet() + private var modifiyTimestamp: Long = 0 + private val cancellationRequested = AtomicBoolean(false) + private val mainThreadHandler = Handler(Looper.getMainLooper()) + + constructor(user: User, file: OCFile, context: Context?) : this( + user, file, null, null, null, context, DownloadType.DOWNLOAD + ) + + fun isMatching(accountName: String?, fileId: Long) = + file.fileId == fileId && user.accountName == accountName + + fun cancelMatchingOperation(accountName: String?, fileId: Long) { + if (isMatching(accountName, fileId)) cancel() + } + + val savePath: String + get() { + file.storagePath?.let { storagePath -> + val parentFile = File(storagePath).parentFile + parentFile?.takeIf { !it.exists() }?.runCatching { + Files.createDirectories(toPath()) + }?.onFailure { + return FileStorageUtils.getDefaultSavePathFor(user.accountName, file) } + val path = File(storagePath) + if (path.canWrite() || parentFile?.canWrite() == true) return path.absolutePath } - File path = new File(file.getStoragePath()); // re-downloads should be done over the original file - if (path.canWrite() || parentFile != null && parentFile.canWrite()) { - return path.getAbsolutePath(); - } + return FileStorageUtils.getDefaultSavePathFor(user.accountName, file) } - return FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), file); - } - - public String getTmpPath() { - return FileStorageUtils.getTemporalPath(user.getAccountName()) + file.getRemotePath(); - } - - public String getTmpFolder() { - return FileStorageUtils.getTemporalPath(user.getAccountName()); - } - - public String getRemotePath() { - return file.getRemotePath(); - } - public String getMimeType() { - String mimeType = file.getMimeType(); - if (TextUtils.isEmpty(mimeType)) { - try { - mimeType = MimeTypeMap.getSingleton() - .getMimeTypeFromExtension( - file.getRemotePath().substring( - file.getRemotePath().lastIndexOf('.') + 1)); - } catch (IndexOutOfBoundsException e) { - Log_OC.e(TAG, "Trying to find out MIME type of a file without extension: " + - file.getRemotePath()); + val tmpPath: String get() = FileStorageUtils.getTemporalPath(user.accountName) + file.remotePath + val tmpFolder: String get() = FileStorageUtils.getTemporalPath(user.accountName) + val remotePath: String get() = file.remotePath + + val mimeType: String + get() { + var mimeType = file.mimeType + if (mimeType.isNullOrEmpty()) { + runCatching { + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( + file.remotePath.substring(file.remotePath.lastIndexOf('.') + 1) + ) + }.onFailure { + Log_OC.e(TAG, "Trying to find out MIME type of a file without extension: ${file.remotePath}") + } } + return mimeType ?: "application/octet-stream" } - if (mimeType == null) { - mimeType = "application/octet-stream"; - } - return mimeType; - } - public long getSize() { - return file.getFileLength(); - } + val size: Long get() = file.fileLength - public long getModificationTimestamp() { - return modificationTimestamp > 0 ? modificationTimestamp : file.getModificationTimestamp(); - } + fun getModificationTimestamp() = + if (modifiyTimestamp > 0) modifiyTimestamp else file.modificationTimestamp - @Override - protected RemoteOperationResult run(OwnCloudClient client) { - /// perform the download + @Suppress("DEPRECATION") + @Deprecated("Deprecated in Java") + override fun run(client: OwnCloudClient): RemoteOperationResult { synchronized(cancellationRequested) { - if (cancellationRequested.get()) { - return new RemoteOperationResult<>(new OperationCancelledException()); - } - } - - final var isValidExtFilename = FileStorageUtils.isValidExtFilename(file.getFileName()); - if (!isValidExtFilename) { - mainThreadHandler.post(() -> ContextExtensionsKt.showToast(context.get(), R.string.download_download_invalid_local_file_name)); - return new RemoteOperationResult<>(RemoteOperationResult.ResultCode.INVALID_CHARACTER_IN_NAME); + if (cancellationRequested.get()) return RemoteOperationResult(OperationCancelledException()) } - Context operationContext = context.get(); - if (operationContext == null) { - return new RemoteOperationResult<>(RemoteOperationResult.ResultCode.UNKNOWN_ERROR); + if (!FileStorageUtils.isValidExtFilename(file.fileName)) { + mainThreadHandler.post { context.get()?.showToast(R.string.download_download_invalid_local_file_name) } + return RemoteOperationResult(RemoteOperationResult.ResultCode.INVALID_CHARACTER_IN_NAME) } - RemoteOperationResult result; - File newFile = null; - boolean moved; + val operationContext = context.get() + ?: return RemoteOperationResult(RemoteOperationResult.ResultCode.UNKNOWN_ERROR) - /// download will be performed to a temporal file, then moved to the final location - File tmpFile = new File(getTmpPath()); + val tmpFile = File(tmpPath) + val (downloadOp, downloadResult) = executeDownload(client, operationContext) - String tmpFolder = getTmpFolder(); + if (!downloadResult.isSuccess) return downloadResult - downloadOperation = new DownloadFileRemoteOperation(file.getRemotePath(), tmpFolder); + modifiyTimestamp = downloadOp.modificationTimestamp + etag = downloadOp.etag if (downloadType == DownloadType.DOWNLOAD) { - dataTransferListeners.forEach(downloadOperation::addDatatransferProgressListener); + File(savePath).also { it.parentFile?.takeIf { p -> !p.exists() }?.mkdirs() } } - NextcloudClient nextcloudClient = OwnCloudClientExtensionsKt.toNextcloudClient(client, operationContext); - result = downloadOperation.execute(nextcloudClient); - - - - if (result.isSuccess()) { - modificationTimestamp = downloadOperation.getModificationTimestamp(); - etag = downloadOperation.getEtag(); - - if (downloadType == DownloadType.DOWNLOAD) { - newFile = new File(getSavePath()); - - if (!newFile.getParentFile().exists() && !newFile.getParentFile().mkdirs()) { - Log_OC.e(TAG, "Unable to create parent folder " + newFile.getParentFile().getAbsolutePath()); - } - } - - // decrypt file - if (file.isEncrypted()) { - FileDataStorageManager fileDataStorageManager = new FileDataStorageManager(user, operationContext.getContentResolver()); - - OCFile parent = fileDataStorageManager.getFileByEncryptedRemotePath(file.getParentRemotePath()); - - Object object = EncryptionUtils.downloadFolderMetadata(parent, - client, - operationContext, - user); - - if (object == null) { - return new RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND); - } - - String keyString; - String nonceString; - String authenticationTagString; - if (object instanceof DecryptedFolderMetadataFile) { - DecryptedFile decryptedFile = ((DecryptedFolderMetadataFile) object) - .getMetadata() - .getFiles() - .get(file.getEncryptedFileName()); - - if (decryptedFile == null) { - return new RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND); - } - - keyString = decryptedFile.getKey(); - nonceString = decryptedFile.getNonce(); - authenticationTagString = decryptedFile.getAuthenticationTag(); - } else { - com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile decryptedFile = - ((DecryptedFolderMetadataFileV1) object) - .getFiles() - .get(file.getEncryptedFileName()); - - if (decryptedFile == null) { - return new RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND); - } - - keyString = decryptedFile.getEncrypted().getKey(); - nonceString = decryptedFile.getInitializationVector(); - authenticationTagString = decryptedFile.getAuthenticationTag(); - } - - byte[] key = decodeStringToBase64Bytes(keyString); - byte[] iv = decodeStringToBase64Bytes(nonceString); - - try { - Cipher cipher = EncryptionUtils.getCipher(Cipher.DECRYPT_MODE, key, iv); - EncryptionUtils.decryptFile(cipher, tmpFile, newFile, authenticationTagString, new ArbitraryDataProviderImpl(operationContext), user); - } catch (Exception e) { - return new RemoteOperationResult(e); - } - } + if (file.isEncrypted) { + handleDecryption(client, operationContext, tmpFile)?.let { return it } + } - if (downloadType == DownloadType.DOWNLOAD && !file.isEncrypted()) { - moved = tmpFile.renameTo(newFile); - boolean isLastModifiedSet = newFile.setLastModified(file.getModificationTimestamp()); - Log_OC.d(TAG, "Last modified set: " + isLastModifiedSet); - if (!moved) { - result = new RemoteOperationResult(RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_MOVED); - } - } else if (downloadType == DownloadType.EXPORT) { - new FileExportUtils().exportFile(file.getFileName(), - file.getMimeType(), - operationContext.getContentResolver(), - null, - tmpFile); - if (!tmpFile.delete()) { - Log_OC.e(TAG, "Deletion of " + tmpFile.getAbsolutePath() + " failed!"); - } - } + val result = when (downloadType) { + DownloadType.DOWNLOAD if !file.isEncrypted -> handleFileMove(tmpFile, downloadResult) + DownloadType.EXPORT -> handleExport(operationContext, tmpFile, downloadResult) + else -> downloadResult } - Log_OC.i(TAG, "Download of " + file.getRemotePath() + " to " + getSavePath() + ": " + - result.getLogMessage()); + Log_OC.i(TAG, "Download of ${file.remotePath} to $savePath: ${result.logMessage}") - return result; + return result } - public void cancel() { - cancellationRequested.set(true); // atomic set; there is no need of synchronizing it - if (downloadOperation != null) { - downloadOperation.cancel(); + private data class DownloadResult( + val operation: DownloadFileRemoteOperation, + val result: RemoteOperationResult + ) + + @Suppress("UNCHECKED_CAST") + private fun executeDownload(client: OwnCloudClient, operationContext: Context): DownloadResult { + val operation = DownloadFileRemoteOperation(file.remotePath, tmpFolder).also { op -> + if (downloadType == DownloadType.DOWNLOAD) { + dataTransferListeners.forEach { op.addDatatransferProgressListener(it) } + } } + val result = operation.execute(client.toNextcloudClient(operationContext)) as RemoteOperationResult + return DownloadResult(operation, result) } + private data class EncryptionKeys(val key: String?, val nonce: String?, val authTag: String?) - public void addDownloadDataTransferProgressListener(OnDatatransferProgressListener listener) { - synchronized (dataTransferListeners) { - dataTransferListeners.add(listener); + private fun extractEncryptionKeys(metadata: Any): EncryptionKeys? = when (metadata) { + is DecryptedFolderMetadataFile -> metadata.metadata.files[file.encryptedFileName]?.let { + EncryptionKeys(it.key, it.nonce, it.authenticationTag) } - } - - public void removeDatatransferProgressListener(OnDatatransferProgressListener listener) { - synchronized (dataTransferListeners) { - dataTransferListeners.remove(listener); + is DecryptedFolderMetadataFileV1 -> metadata.files[file.encryptedFileName]?.let { + EncryptionKeys(it.encrypted.key, it.initializationVector, it.authenticationTag) + } + else -> null + } + + private fun handleDecryption( + client: OwnCloudClient, + operationContext: Context, + tmpFile: File + ): RemoteOperationResult? { + val fileDataStorageManager = FileDataStorageManager(user, operationContext.contentResolver) + val parent = fileDataStorageManager.getFileByEncryptedRemotePath(file.parentRemotePath) + val metadata = EncryptionUtils.downloadFolderMetadata(parent, client, operationContext, user) + ?: return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) + + val (keyString, nonceString, authenticationTagString) = extractEncryptionKeys(metadata) + ?: return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) + + val key = EncryptionUtils.decodeStringToBase64Bytes(keyString) + val iv = EncryptionUtils.decodeStringToBase64Bytes(nonceString) + + return runCatching { + val cipher = EncryptionUtils.getCipher(Cipher.DECRYPT_MODE, key, iv) + EncryptionUtils.decryptFile(cipher, tmpFile, File(savePath), authenticationTagString, ArbitraryDataProviderImpl(operationContext), user) + }.fold( + onSuccess = { null }, + onFailure = { RemoteOperationResult(it as? Exception ?: Exception(it)) } + ) + } + + private fun handleFileMove(tmpFile: File, currentResult: RemoteOperationResult): RemoteOperationResult { + val newFile = File(savePath) + return if (tmpFile.renameTo(newFile)) { + newFile.setLastModified(file.modificationTimestamp) + .also { Log_OC.d(TAG, "Last modified set: $it") } + currentResult + } else { + RemoteOperationResult(RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_MOVED) } } - public User getUser() { - return this.user; - } - - public OCFile getFile() { - return this.file; - } - - public String getBehaviour() { - return this.behaviour; - } - - public String getEtag() { - return this.etag; + private fun handleExport( + operationContext: Context, + tmpFile: File, + currentResult: RemoteOperationResult + ): RemoteOperationResult { + FileExportUtils().exportFile(file.fileName, file.mimeType, operationContext.contentResolver, null, tmpFile) + if (!tmpFile.delete()) Log_OC.e(TAG, "Deletion of ${tmpFile.absolutePath} failed!") + return currentResult } - public String getActivityName() { - return this.activityName; + fun cancel() { + cancellationRequested.set(true) } - public String getPackageName() { - return this.packageName; + fun addDownloadDataTransferProgressListener(listener: OnDatatransferProgressListener?) { + synchronized(dataTransferListeners) { dataTransferListeners.add(listener) } } - public DownloadType getDownloadType() { - return downloadType; + fun removeDatatransferProgressListener(listener: OnDatatransferProgressListener?) { + synchronized(dataTransferListeners) { dataTransferListeners.remove(listener) } } - public void setDownloadType(DownloadType type) { - downloadType = type; + companion object { + private val TAG = DownloadFileOperation::class.java.simpleName } } From 69d74a4efa5f713ebba2c138d8efc219101bb51d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 28 May 2026 09:53:04 +0300 Subject: [PATCH 3/4] wip Signed-off-by: alperozturk96 --- .../nextcloud/client/jobs/ContactsBackupIT.kt | 4 +-- .../operations/DownloadFileOperation.kt | 31 ++++++++++++------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt b/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt index bb68dfcfd153..79197a12a034 100644 --- a/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt @@ -115,9 +115,9 @@ class ContactsBackupIT : AbstractOnServerIT() { fail("ocFile.storagePath cannot be null") } - assertTrue(DownloadFileOperation(user, ocFile, targetContext).execute(client).isSuccess) + assertTrue(DownloadFileOperation(user, ocFile!!, targetContext).execute(client).isSuccess) - val file = ocFile?.storagePath?.let { File(it) } + val file = ocFile.storagePath?.let { File(it) } if (file == null) { fail("file cannot be null") } diff --git a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt index 217a464d2d7a..b89ac04c2ea0 100644 --- a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt @@ -35,6 +35,7 @@ import com.owncloud.android.utils.EncryptionUtils import com.owncloud.android.utils.FileExportUtils import com.owncloud.android.utils.FileStorageUtils import java.io.File +import java.io.IOException import java.lang.ref.WeakReference import java.nio.file.Files import java.util.concurrent.atomic.AtomicBoolean @@ -55,7 +56,7 @@ class DownloadFileOperation( private val context = WeakReference(context) private val dataTransferListeners: MutableSet = HashSet() - private var modifiyTimestamp: Long = 0 + private var timestampForModification: Long = 0 private val cancellationRequested = AtomicBoolean(false) private val mainThreadHandler = Handler(Looper.getMainLooper()) @@ -107,7 +108,7 @@ class DownloadFileOperation( val size: Long get() = file.fileLength fun getModificationTimestamp() = - if (modifiyTimestamp > 0) modifiyTimestamp else file.modificationTimestamp + if (timestampForModification > 0) timestampForModification else file.modificationTimestamp @Suppress("DEPRECATION") @Deprecated("Deprecated in Java") @@ -129,13 +130,9 @@ class DownloadFileOperation( if (!downloadResult.isSuccess) return downloadResult - modifiyTimestamp = downloadOp.modificationTimestamp + timestampForModification = downloadOp.modificationTimestamp etag = downloadOp.etag - if (downloadType == DownloadType.DOWNLOAD) { - File(savePath).also { it.parentFile?.takeIf { p -> !p.exists() }?.mkdirs() } - } - if (file.isEncrypted) { handleDecryption(client, operationContext, tmpFile)?.let { return it } } @@ -147,7 +144,6 @@ class DownloadFileOperation( } Log_OC.i(TAG, "Download of ${file.remotePath} to $savePath: ${result.logMessage}") - return result } @@ -179,6 +175,7 @@ class DownloadFileOperation( else -> null } + @Suppress("DEPRECATION") private fun handleDecryption( client: OwnCloudClient, operationContext: Context, @@ -197,7 +194,14 @@ class DownloadFileOperation( return runCatching { val cipher = EncryptionUtils.getCipher(Cipher.DECRYPT_MODE, key, iv) - EncryptionUtils.decryptFile(cipher, tmpFile, File(savePath), authenticationTagString, ArbitraryDataProviderImpl(operationContext), user) + EncryptionUtils.decryptFile( + cipher, + tmpFile, + File(savePath), + authenticationTagString, + ArbitraryDataProviderImpl(operationContext), + user + ) }.fold( onSuccess = { null }, onFailure = { RemoteOperationResult(it as? Exception ?: Exception(it)) } @@ -206,11 +210,16 @@ class DownloadFileOperation( private fun handleFileMove(tmpFile: File, currentResult: RemoteOperationResult): RemoteOperationResult { val newFile = File(savePath) - return if (tmpFile.renameTo(newFile)) { + newFile.parentFile?.takeIf { !it.exists() }?.also { + if (!it.mkdirs()) Log_OC.e(TAG, "Unable to create parent folder ${it.absolutePath}") + } + return try { + Files.move(tmpFile.toPath(), newFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING) newFile.setLastModified(file.modificationTimestamp) .also { Log_OC.d(TAG, "Last modified set: $it") } currentResult - } else { + } catch (e: IOException) { + Log_OC.e(TAG, "Failed to move file to ${newFile.absolutePath}: ${e.message}") RemoteOperationResult(RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_MOVED) } } From f1f73ad77f38167679a00881dccc0a73eef8b014 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 28 May 2026 12:43:07 +0300 Subject: [PATCH 4/4] improve file download Signed-off-by: alperozturk96 --- .../operations/DownloadFileOperation.kt | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt index b89ac04c2ea0..a68112e097db 100644 --- a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.kt @@ -41,6 +41,7 @@ import java.nio.file.Files import java.util.concurrent.atomic.AtomicBoolean import javax.crypto.Cipher +@Suppress("LongParameterList", "ReturnCount") class DownloadFileOperation( val user: User, val file: OCFile, @@ -61,11 +62,16 @@ class DownloadFileOperation( private val mainThreadHandler = Handler(Looper.getMainLooper()) constructor(user: User, file: OCFile, context: Context?) : this( - user, file, null, null, null, context, DownloadType.DOWNLOAD + user, + file, + null, + null, + null, + context, + DownloadType.DOWNLOAD ) - fun isMatching(accountName: String?, fileId: Long) = - file.fileId == fileId && user.accountName == accountName + fun isMatching(accountName: String?, fileId: Long) = file.fileId == fileId && user.accountName == accountName fun cancelMatchingOperation(accountName: String?, fileId: Long) { if (isMatching(accountName, fileId)) cancel() @@ -169,9 +175,11 @@ class DownloadFileOperation( is DecryptedFolderMetadataFile -> metadata.metadata.files[file.encryptedFileName]?.let { EncryptionKeys(it.key, it.nonce, it.authenticationTag) } + is DecryptedFolderMetadataFileV1 -> metadata.files[file.encryptedFileName]?.let { EncryptionKeys(it.encrypted.key, it.initializationVector, it.authenticationTag) } + else -> null } @@ -213,8 +221,14 @@ class DownloadFileOperation( newFile.parentFile?.takeIf { !it.exists() }?.also { if (!it.mkdirs()) Log_OC.e(TAG, "Unable to create parent folder ${it.absolutePath}") } + + val tempFilePath = tmpFile.toPath() + val newFilePath = newFile.toPath() + + Log_OC.d(TAG, "trying to move: $tempFilePath to $newFilePath") + return try { - Files.move(tmpFile.toPath(), newFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING) + Files.move(tempFilePath, newFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING) newFile.setLastModified(file.modificationTimestamp) .also { Log_OC.d(TAG, "Last modified set: $it") } currentResult