From 555a77862f10185922b8aa646316b089a3d7a1e1 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 9 Jun 2026 09:46:40 +0200 Subject: [PATCH 1/6] remove file size check while downloading Signed-off-by: alperozturk96 --- .../files/DownloadFileRemoteOperation.java | 84 ++++++++----------- 1 file changed, 37 insertions(+), 47 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.java b/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.java index 8c23d3738..d8e0eefc5 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.java +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.java @@ -38,14 +38,14 @@ public class DownloadFileRemoteOperation extends RemoteOperation { private static final String TAG = DownloadFileRemoteOperation.class.getSimpleName(); - private Set mDataTransferListeners = new HashSet<>(); + private final Set mDataTransferListeners = new HashSet<>(); private final AtomicBoolean mCancellationRequested = new AtomicBoolean(false); private long modificationTimestamp = 0; private String eTag = ""; private GetMethod getMethod; - private String remotePath; - private String temporalFolderPath; + private final String remotePath; + private final String temporalFolderPath; /** * @param remotePath which file to download @@ -58,21 +58,27 @@ public DownloadFileRemoteOperation(String remotePath, String temporalFolderPath) @Override public RemoteOperationResult run(NextcloudClient client) { - RemoteOperationResult result; - - /// download will be performed to a temporal file, then moved to the final location + RemoteOperationResult result; File tmpFile = new File(getTmpPath()); - /// perform the download try { - tmpFile.getParentFile().mkdirs(); + final var parentFile = tmpFile.getParentFile(); + if (parentFile == null) { + return new RemoteOperationResult<>(new Exception("parent file is null")); + } + + boolean isTempFileCreated = parentFile.mkdirs(); + if (isTempFileCreated) { + Log_OC.d(TAG, "temp file created"); + } + int status = downloadFile(client, tmpFile); - result = new RemoteOperationResult(isSuccess(status), getMethod); + result = new RemoteOperationResult<>(isSuccess(status), getMethod); Log_OC.i(TAG, "Download of " + remotePath + " to " + getTmpPath() + ": " + result.getLogMessage()); } catch (Exception e) { - result = new RemoteOperationResult(e); + result = new RemoteOperationResult<>(e); Log_OC.e(TAG, "Download of " + remotePath + " to " + getTmpPath() + ": " + result.getLogMessage(), e); } @@ -83,26 +89,26 @@ public RemoteOperationResult run(NextcloudClient client) { private int downloadFile(NextcloudClient client, File targetFile) throws IOException, OperationCancelledException, CreateLocalFileException { int status; - boolean savedFile = false; getMethod = new GetMethod(client.getFilesDavUri(remotePath), false); Iterator it; FileOutputStream fos = null; try { status = client.execute(getMethod); + if (isSuccess(status)) { try { - targetFile.createNewFile(); + boolean isTargetFileCreated = targetFile.createNewFile(); + if (isTargetFileCreated) { + Log_OC.i(TAG, "target file is created"); + } } catch (IOException | SecurityException ex) { Log_OC.e(TAG, "Error creating file " + targetFile.getAbsolutePath(), ex); throw new CreateLocalFileException(targetFile.getPath(), ex); } + BufferedInputStream bis = new BufferedInputStream(getMethod.getResponseBodyAsStream()); fos = new FileOutputStream(targetFile); - long transferred = 0; - - String contentLength = getMethod.getResponseHeader("Content-Length"); - long totalToTransfer = (contentLength != null) ? Long.parseLong(contentLength) : 0; byte[] bytes = new byte[4096]; int readResult; @@ -112,50 +118,34 @@ private int downloadFile(NextcloudClient client, File targetFile) throws IOExcep throw new OperationCancelledException(); } } + fos.write(bytes, 0, readResult); - transferred += readResult; + synchronized (mDataTransferListeners) { it = mDataTransferListeners.iterator(); - while (it.hasNext()) { - it.next().onTransferProgress(readResult, transferred, totalToTransfer, - targetFile.getName()); - } } } - // Check if the file is completed - // if transfer-encoding: chunked we cannot check if the file is complete - String transferEncodingHeader = getMethod.getResponseHeader("Transfer-Encoding"); - boolean transferEncoding = false; - if (transferEncodingHeader != null) { - transferEncoding = "chunked".equals(transferEncodingHeader); + + String modificationTime = getMethod.getResponseHeader("Last-Modified"); + if (modificationTime == null) { + modificationTime = getMethod.getResponseHeader("last-modified"); } - - if (transferred == totalToTransfer || transferEncoding) { - savedFile = true; - String modificationTime = getMethod.getResponseHeader("Last-Modified"); - if (modificationTime == null) { - modificationTime = getMethod.getResponseHeader("last-modified"); - } - if (modificationTime != null) { - Date d = WebdavUtils.parseResponseDate(modificationTime); - modificationTimestamp = (d != null) ? d.getTime() : 0; - } else { - Log_OC.e(TAG, "Could not read modification time from response downloading " + remotePath); - } - eTag = WebdavUtils.getEtagFromResponse(getMethod); - if (eTag.length() == 0) { - Log_OC.e(TAG, "Could not read eTag from response downloading " + remotePath); - } + if (modificationTime != null) { + Date d = WebdavUtils.parseResponseDate(modificationTime); + modificationTimestamp = (d != null) ? d.getTime() : 0; + } else { + Log_OC.e(TAG, "Could not read modification time from response downloading " + remotePath); + } + eTag = WebdavUtils.getEtagFromResponse(getMethod); + if (eTag.isEmpty()) { + Log_OC.e(TAG, "Could not read eTag from response downloading " + remotePath); } } } finally { if (fos != null) fos.close(); - if (!savedFile && targetFile.exists()) { - targetFile.delete(); - } getMethod.releaseConnection(); // let the connection available for other methods } return status; From 50071a9182a95bfcc021cfea5a1c2f98045ed521 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 9 Jun 2026 09:54:40 +0200 Subject: [PATCH 2/6] Rename .java to .kt Signed-off-by: alperozturk96 --- ...oadFileRemoteOperation.java => DownloadFileRemoteOperation.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename library/src/main/java/com/owncloud/android/lib/resources/files/{DownloadFileRemoteOperation.java => DownloadFileRemoteOperation.kt} (100%) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.java b/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.kt similarity index 100% rename from library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.java rename to library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.kt From ff38d33d71e9199d1ce3d1e22f590721b6583a79 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 9 Jun 2026 09:54:40 +0200 Subject: [PATCH 3/6] convert to kotlin Signed-off-by: alperozturk96 --- .../files/DownloadFileRemoteOperation.kt | 231 ++++++++---------- 1 file changed, 104 insertions(+), 127 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.kt index d8e0eefc5..acdfbac54 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.kt @@ -1,185 +1,162 @@ /* * Nextcloud Android Library * - * SPDX-FileCopyrightText: 2015 ownCloud Inc. + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: MIT */ -package com.owncloud.android.lib.resources.files; - -import com.nextcloud.common.NextcloudClient; -import com.nextcloud.operations.GetMethod; -import com.owncloud.android.lib.common.network.OnDatatransferProgressListener; -import com.owncloud.android.lib.common.network.WebdavUtils; -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 org.apache.commons.httpclient.HttpStatus; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Date; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Remote operation performing the download of a remote file in the ownCloud server. - * - * @author David A. Velasco - * @author masensio - */ - -public class DownloadFileRemoteOperation extends RemoteOperation { - - private static final String TAG = DownloadFileRemoteOperation.class.getSimpleName(); - - private final Set mDataTransferListeners = new HashSet<>(); - private final AtomicBoolean mCancellationRequested = new AtomicBoolean(false); - private long modificationTimestamp = 0; - private String eTag = ""; - private GetMethod getMethod; - - private final String remotePath; - private final String temporalFolderPath; - - /** - * @param remotePath which file to download - * @param temporalFolderPath temporal folder where file is stored, to avoid conflicts it use full remote path - */ - public DownloadFileRemoteOperation(String remotePath, String temporalFolderPath) { - this.remotePath = remotePath; - this.temporalFolderPath = temporalFolderPath; - } - - @Override - public RemoteOperationResult run(NextcloudClient client) { - RemoteOperationResult result; - File tmpFile = new File(getTmpPath()); +package com.owncloud.android.lib.resources.files + +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.GetMethod +import com.owncloud.android.lib.common.network.OnDatatransferProgressListener +import com.owncloud.android.lib.common.network.WebdavUtils +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 org.apache.commons.httpclient.HttpStatus +import java.io.BufferedInputStream +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.concurrent.atomic.AtomicBoolean + +class DownloadFileRemoteOperation(private val remotePath: String, private val temporalFolderPath: String?) : + RemoteOperation() { + private val dataTransferListeners: MutableSet = + HashSet() + private val cancellationRequested = AtomicBoolean(false) + var modificationTimestamp: Long = 0 + private set + var etag: String = "" + private set + + override fun run(client: NextcloudClient): RemoteOperationResult { + var result: RemoteOperationResult + val tmpFile = File(this.tmpPath) try { - final var parentFile = tmpFile.getParentFile(); - if (parentFile == null) { - return new RemoteOperationResult<>(new Exception("parent file is null")); - } + val parentFile = + tmpFile.getParentFile() ?: return RemoteOperationResult(Exception("parent file is null")) - boolean isTempFileCreated = parentFile.mkdirs(); + val isTempFileCreated = parentFile.mkdirs() if (isTempFileCreated) { - Log_OC.d(TAG, "temp file created"); + Log_OC.d(TAG, "temp file created") } - int status = downloadFile(client, tmpFile); - result = new RemoteOperationResult<>(isSuccess(status), getMethod); - Log_OC.i(TAG, "Download of " + remotePath + " to " + getTmpPath() + ": " + - result.getLogMessage()); - - } catch (Exception e) { - result = new RemoteOperationResult<>(e); - Log_OC.e(TAG, "Download of " + remotePath + " to " + getTmpPath() + ": " + - result.getLogMessage(), e); + val getMethod = GetMethod(client.getFilesDavUri(remotePath), false) + val status = downloadFile(getMethod, client, tmpFile) + result = RemoteOperationResult(isSuccess(status), getMethod) + Log_OC.i( + TAG, "Download of " + remotePath + " to " + this.tmpPath + ": " + + result.getLogMessage() + ) + } catch (e: Exception) { + result = RemoteOperationResult(e) + Log_OC.e( + TAG, "Download of " + remotePath + " to " + this.tmpPath + ": " + + result.getLogMessage(), e + ) } - return result; + return result } + @Throws(IOException::class, OperationCancelledException::class, CreateLocalFileException::class) + private fun downloadFile(getMethod: GetMethod, client: NextcloudClient, targetFile: File): Int { + var status: Int + var it: MutableIterator? - private int downloadFile(NextcloudClient client, File targetFile) throws IOException, OperationCancelledException, CreateLocalFileException { - int status; - getMethod = new GetMethod(client.getFilesDavUri(remotePath), false); - Iterator it; - - FileOutputStream fos = null; + var fos: FileOutputStream? = null try { - status = client.execute(getMethod); + status = client.execute(getMethod) if (isSuccess(status)) { try { - boolean isTargetFileCreated = targetFile.createNewFile(); + val isTargetFileCreated = targetFile.createNewFile() if (isTargetFileCreated) { - Log_OC.i(TAG, "target file is created"); + Log_OC.i(TAG, "target file is created") } - } catch (IOException | SecurityException ex) { - Log_OC.e(TAG, "Error creating file " + targetFile.getAbsolutePath(), ex); - throw new CreateLocalFileException(targetFile.getPath(), ex); + } catch (ex: IOException) { + Log_OC.e(TAG, "Error creating file " + targetFile.absolutePath, ex) + throw CreateLocalFileException(targetFile.path, ex) + } catch (ex: SecurityException) { + Log_OC.e(TAG, "Error creating file " + targetFile.absolutePath, ex) + throw CreateLocalFileException(targetFile.path, ex) } - BufferedInputStream bis = new BufferedInputStream(getMethod.getResponseBodyAsStream()); - fos = new FileOutputStream(targetFile); + val bis = BufferedInputStream(getMethod?.getResponseBodyAsStream()) + fos = FileOutputStream(targetFile) - byte[] bytes = new byte[4096]; - int readResult; - while ((readResult = bis.read(bytes)) != -1) { - synchronized (mCancellationRequested) { - if (mCancellationRequested.get()) { - throw new OperationCancelledException(); + val bytes = ByteArray(4096) + var readResult: Int + while ((bis.read(bytes).also { readResult = it }) != -1) { + synchronized(cancellationRequested) { + if (cancellationRequested.get()) { + throw OperationCancelledException() } } - fos.write(bytes, 0, readResult); + fos.write(bytes, 0, readResult) - synchronized (mDataTransferListeners) { - it = mDataTransferListeners.iterator(); + synchronized(dataTransferListeners) { + it = dataTransferListeners.iterator() } } - - String modificationTime = getMethod.getResponseHeader("Last-Modified"); + var modificationTime = getMethod.getResponseHeader("Last-Modified") if (modificationTime == null) { - modificationTime = getMethod.getResponseHeader("last-modified"); + modificationTime = getMethod.getResponseHeader("last-modified") } if (modificationTime != null) { - Date d = WebdavUtils.parseResponseDate(modificationTime); - modificationTimestamp = (d != null) ? d.getTime() : 0; + val d = WebdavUtils.parseResponseDate(modificationTime) + modificationTimestamp = d?.time ?: 0 } else { - Log_OC.e(TAG, "Could not read modification time from response downloading " + remotePath); + Log_OC.e( + TAG, + "Could not read modification time from response downloading $remotePath" + ) } - eTag = WebdavUtils.getEtagFromResponse(getMethod); - if (eTag.isEmpty()) { - Log_OC.e(TAG, "Could not read eTag from response downloading " + remotePath); + this.etag = WebdavUtils.getEtagFromResponse(getMethod) + if (etag.isEmpty()) { + Log_OC.e(TAG, "Could not read eTag from response downloading $remotePath") } } } finally { - if (fos != null) fos.close(); - getMethod.releaseConnection(); // let the connection available for other methods + fos?.close() + + // let the connection available for other methods + getMethod.releaseConnection() } - return status; + return status } - private boolean isSuccess(int status) { - return (status == HttpStatus.SC_OK); + private fun isSuccess(status: Int): Boolean { + return (status == HttpStatus.SC_OK) } - private String getTmpPath() { - return temporalFolderPath + remotePath; - } + private val tmpPath: String + get() = temporalFolderPath + remotePath - public void addDatatransferProgressListener(OnDatatransferProgressListener listener) { - synchronized (mDataTransferListeners) { - mDataTransferListeners.add(listener); + fun addDatatransferProgressListener(listener: OnDatatransferProgressListener?) { + synchronized(dataTransferListeners) { + dataTransferListeners.add(listener) } } - public void removeDatatransferProgressListener(OnDatatransferProgressListener listener) { - synchronized (mDataTransferListeners) { - mDataTransferListeners.remove(listener); + fun removeDatatransferProgressListener(listener: OnDatatransferProgressListener?) { + synchronized(dataTransferListeners) { + dataTransferListeners.remove(listener) } } - public void cancel() { - mCancellationRequested.set(true); // atomic set; there is no need of synchronizing it - } - - public long getModificationTimestamp() { - return modificationTimestamp; + fun cancel() { + cancellationRequested.set(true) } - public String getEtag() { - return eTag; + companion object { + private val TAG: String = DownloadFileRemoteOperation::class.java.getSimpleName() } } From bdf78f9b66882bc751a752627d97a0b22951b982 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 9 Jun 2026 10:05:49 +0200 Subject: [PATCH 4/6] simplify logic Signed-off-by: alperozturk96 --- .../files/DownloadFileRemoteOperation.kt | 193 +++++++++--------- .../lib/sampleclient/MainActivity.java | 10 +- 2 files changed, 100 insertions(+), 103 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.kt index acdfbac54..836fc087c 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.kt @@ -6,6 +6,8 @@ */ package com.owncloud.android.lib.resources.files +import android.os.Build +import androidx.annotation.RequiresApi import com.nextcloud.common.NextcloudClient import com.nextcloud.operations.GetMethod import com.owncloud.android.lib.common.network.OnDatatransferProgressListener @@ -16,147 +18,136 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import org.apache.commons.httpclient.HttpStatus import java.io.BufferedInputStream -import java.io.File -import java.io.FileOutputStream import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths import java.util.concurrent.atomic.AtomicBoolean -class DownloadFileRemoteOperation(private val remotePath: String, private val temporalFolderPath: String?) : - RemoteOperation() { - private val dataTransferListeners: MutableSet = - HashSet() +@Suppress("NestedBlockDepth", "TooGenericExceptionCaught", "ThrowsCount") +@RequiresApi(Build.VERSION_CODES.O) +class DownloadFileRemoteOperation( + private val remotePath: String, + private val temporalFolderPath: String? +) : RemoteOperation() { + private val dataTransferListeners: MutableSet = HashSet() private val cancellationRequested = AtomicBoolean(false) var modificationTimestamp: Long = 0 private set var etag: String = "" private set + @Suppress("DEPRECATION") override fun run(client: NextcloudClient): RemoteOperationResult { - var result: RemoteOperationResult - val tmpFile = File(this.tmpPath) - - try { - val parentFile = - tmpFile.getParentFile() ?: return RemoteOperationResult(Exception("parent file is null")) - - val isTempFileCreated = parentFile.mkdirs() - if (isTempFileCreated) { - Log_OC.d(TAG, "temp file created") - } - + val targetPath = Paths.get(tmpPath) + return try { + val parent = targetPath.parent ?: throw IOException("No parent directory for: $targetPath") + Files.createDirectories(parent) val getMethod = GetMethod(client.getFilesDavUri(remotePath), false) - val status = downloadFile(getMethod, client, tmpFile) - result = RemoteOperationResult(isSuccess(status), getMethod) - Log_OC.i( - TAG, "Download of " + remotePath + " to " + this.tmpPath + ": " + - result.getLogMessage() - ) + val status = downloadFile(getMethod, client, targetPath) + RemoteOperationResult(isSuccess(status), getMethod).also { + Log_OC.i(TAG, "Download of $remotePath to $targetPath: ${it.logMessage}") + } } catch (e: Exception) { - result = RemoteOperationResult(e) - Log_OC.e( - TAG, "Download of " + remotePath + " to " + this.tmpPath + ": " + - result.getLogMessage(), e - ) + RemoteOperationResult(e).also { + Log_OC.e(TAG, "Download of $remotePath to $targetPath: ${it.logMessage}", e) + } } - - return result } + // region private methods @Throws(IOException::class, OperationCancelledException::class, CreateLocalFileException::class) - private fun downloadFile(getMethod: GetMethod, client: NextcloudClient, targetFile: File): Int { - var status: Int - var it: MutableIterator? + private fun downloadFile( + getMethod: GetMethod, + client: NextcloudClient, + targetPath: Path + ): Int { + val status = client.execute(getMethod) + if (!isSuccess(status)) { + getMethod.releaseConnection() + return status + } - var fos: FileOutputStream? = null try { - status = client.execute(getMethod) - - if (isSuccess(status)) { - try { - val isTargetFileCreated = targetFile.createNewFile() - if (isTargetFileCreated) { - Log_OC.i(TAG, "target file is created") - } - } catch (ex: IOException) { - Log_OC.e(TAG, "Error creating file " + targetFile.absolutePath, ex) - throw CreateLocalFileException(targetFile.path, ex) - } catch (ex: SecurityException) { - Log_OC.e(TAG, "Error creating file " + targetFile.absolutePath, ex) - throw CreateLocalFileException(targetFile.path, ex) - } - - val bis = BufferedInputStream(getMethod?.getResponseBodyAsStream()) - fos = FileOutputStream(targetFile) - - val bytes = ByteArray(4096) - var readResult: Int - while ((bis.read(bytes).also { readResult = it }) != -1) { - synchronized(cancellationRequested) { - if (cancellationRequested.get()) { - throw OperationCancelledException() - } - } + writeResponseToFile(getMethod, targetPath) + readMetadata(getMethod) + } finally { + getMethod.releaseConnection() + } - fos.write(bytes, 0, readResult) + return status + } - synchronized(dataTransferListeners) { - it = dataTransferListeners.iterator() - } - } + private fun writeResponseToFile( + getMethod: GetMethod, + targetPath: Path + ) { + val responseStream = + getMethod.getResponseBodyAsStream() + ?: throw IOException("Empty response body for $remotePath") + + val outputStream = + try { + Files.newOutputStream(targetPath) + } catch (ex: IOException) { + Log_OC.e(TAG, "Error creating file $targetPath", ex) + throw CreateLocalFileException(targetPath.toString(), ex) + } catch (ex: SecurityException) { + Log_OC.e(TAG, "Error creating file $targetPath", ex) + throw CreateLocalFileException(targetPath.toString(), ex) + } - var modificationTime = getMethod.getResponseHeader("Last-Modified") - if (modificationTime == null) { - modificationTime = getMethod.getResponseHeader("last-modified") + BufferedInputStream(responseStream).use { bis -> + outputStream.use { fos -> + val buffer = ByteArray(BUFFER_SIZE) + var bytesRead: Int + while (bis.read(buffer).also { bytesRead = it } != -1) { + if (cancellationRequested.get()) throw OperationCancelledException() + fos.write(buffer, 0, bytesRead) } + } + } + } - if (modificationTime != null) { - val d = WebdavUtils.parseResponseDate(modificationTime) - modificationTimestamp = d?.time ?: 0 - } else { - Log_OC.e( - TAG, - "Could not read modification time from response downloading $remotePath" - ) - } + private fun readMetadata(getMethod: GetMethod) { + val modificationTime = + getMethod.getResponseHeader("Last-Modified") + ?: getMethod.getResponseHeader("last-modified") - this.etag = WebdavUtils.getEtagFromResponse(getMethod) - if (etag.isEmpty()) { - Log_OC.e(TAG, "Could not read eTag from response downloading $remotePath") - } - } - } finally { - fos?.close() + if (modificationTime != null) { + modificationTimestamp = WebdavUtils.parseResponseDate(modificationTime)?.time ?: 0 + } else { + Log_OC.e(TAG, "Could not read modification time from response downloading $remotePath") + } - // let the connection available for other methods - getMethod.releaseConnection() + etag = WebdavUtils.getEtagFromResponse(getMethod) + if (etag.isEmpty()) { + Log_OC.e(TAG, "Could not read eTag from response downloading $remotePath") } - return status } - private fun isSuccess(status: Int): Boolean { - return (status == HttpStatus.SC_OK) - } + private fun isSuccess(status: Int) = (status == HttpStatus.SC_OK) private val tmpPath: String get() = temporalFolderPath + remotePath + // endregion - fun addDatatransferProgressListener(listener: OnDatatransferProgressListener?) { - synchronized(dataTransferListeners) { - dataTransferListeners.add(listener) - } + // region public methods + fun addProgressListener(listener: OnDatatransferProgressListener) { + synchronized(dataTransferListeners) { dataTransferListeners.add(listener) } } - fun removeDatatransferProgressListener(listener: OnDatatransferProgressListener?) { - synchronized(dataTransferListeners) { - dataTransferListeners.remove(listener) - } + fun removeProgressListener(listener: OnDatatransferProgressListener) { + synchronized(dataTransferListeners) { dataTransferListeners.remove(listener) } } fun cancel() { cancellationRequested.set(true) } + // endregion companion object { - private val TAG: String = DownloadFileRemoteOperation::class.java.getSimpleName() + private val TAG = DownloadFileRemoteOperation::class.java.simpleName + private const val BUFFER_SIZE = 4096 } } diff --git a/sample_client/src/main/java/com/owncloud/android/lib/sampleclient/MainActivity.java b/sample_client/src/main/java/com/owncloud/android/lib/sampleclient/MainActivity.java index b5532b796..b334fc185 100644 --- a/sample_client/src/main/java/com/owncloud/android/lib/sampleclient/MainActivity.java +++ b/sample_client/src/main/java/com/owncloud/android/lib/sampleclient/MainActivity.java @@ -14,6 +14,7 @@ import android.content.res.AssetManager; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.util.Log; @@ -22,6 +23,8 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.RequiresApi; + import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.OwnCloudClientFactory; import com.owncloud.android.lib.common.OwnCloudCredentialsFactory; @@ -126,7 +129,9 @@ public void onClickHandler(View button) { startRemoteDeletion(); break; case R.id.button_download: - startDownload(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startDownload(); + } break; case R.id.button_delete_local: startLocalDeletion(); @@ -169,6 +174,7 @@ private void startRemoteDeletion() { removeOperation.execute(mClient, this, mHandler); } + @RequiresApi(api = Build.VERSION_CODES.O) private void startDownload() { File downFolder = new File(getCacheDir(), getString(R.string.download_folder_path)); downFolder.mkdir(); @@ -176,7 +182,7 @@ private void startDownload() { File fileToUpload = upFolder.listFiles()[0]; String remotePath = FileUtils.PATH_SEPARATOR + fileToUpload.getName(); DownloadFileRemoteOperation downloadOperation = new DownloadFileRemoteOperation(remotePath, downFolder.getAbsolutePath()); - downloadOperation.addDatatransferProgressListener(this); + downloadOperation.addProgressListener(this); downloadOperation.execute(mClient, this, mHandler); } From 13e22428e727364ff3c1b6368ec4d3eadcf85d21 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 9 Jun 2026 17:04:29 +0200 Subject: [PATCH 5/6] add tests Signed-off-by: alperozturk96 --- .../files/DownloadFileRemoteOperationIT.kt | 140 +++++++++++++++++- 1 file changed, 136 insertions(+), 4 deletions(-) diff --git a/library/src/androidTest/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperationIT.kt b/library/src/androidTest/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperationIT.kt index 94840010b..0485f2993 100644 --- a/library/src/androidTest/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperationIT.kt +++ b/library/src/androidTest/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperationIT.kt @@ -1,38 +1,170 @@ /* * Nextcloud Android Library * - * SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2021-2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2021 Tobias Kaminsky * SPDX-License-Identifier: MIT */ package com.owncloud.android.lib.resources.files import com.owncloud.android.AbstractIT +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertSame import org.junit.Assert.assertTrue import org.junit.Test import java.io.File +@Suppress("Detekt.MagicNumber") class DownloadFileRemoteOperationIT : AbstractIT() { + private val cacheDir get() = context.externalCacheDir?.absolutePath + @Test fun download() { val filePath = createFile("download") val remotePath = "/download.jpg" assertTrue( - @Suppress("Detekt.MagicNumber") UploadFileRemoteOperation(filePath, remotePath, "image/jpg", 1464818400) .execute(client) .isSuccess ) assertTrue( - DownloadFileRemoteOperation(remotePath, context.externalCacheDir?.absolutePath) + DownloadFileRemoteOperation(remotePath, cacheDir) .execute(nextcloudClient) .isSuccess ) val oldFile = File(filePath) - val newFile = File(context.externalCacheDir?.absolutePath + remotePath) + val newFile = File(cacheDir + remotePath) assertSame(oldFile.length(), newFile.length()) } + + @Test + fun downloadLargeFile() { + val filePath = createFile("large_download", 1000) + val remotePath = "/large_download.txt" + assertTrue( + UploadFileRemoteOperation(filePath, remotePath, "text/plain", RANDOM_MTIME) + .execute(client) + .isSuccess + ) + + assertTrue( + DownloadFileRemoteOperation(remotePath, cacheDir) + .execute(nextcloudClient) + .isSuccess + ) + + val originalFile = File(filePath) + val downloadedFile = File(cacheDir + remotePath) + assertEquals(originalFile.length(), downloadedFile.length()) + } + + @Test + fun downloadNonExistentFile() { + val result = + DownloadFileRemoteOperation("/nonexistent_file_12345.txt", cacheDir) + .execute(nextcloudClient) + + assertFalse(result.isSuccess) + } + + @Test + fun downloadAndVerifyMetadata() { + val filePath = createFile("metadata_download") + val remotePath = "/metadata_download.jpg" + assertTrue( + UploadFileRemoteOperation(filePath, remotePath, "image/jpg", RANDOM_MTIME) + .execute(client) + .isSuccess + ) + + val operation = DownloadFileRemoteOperation(remotePath, cacheDir) + assertTrue(operation.execute(nextcloudClient).isSuccess) + + assertTrue("ETag should not be empty after download", operation.etag.isNotEmpty()) + assertTrue("Modification timestamp should be positive after download", operation.modificationTimestamp > 0) + } + + @Test + fun downloadMultipleFiles() { + val filePath1 = createFile("multi_download1") + val remotePath1 = "/multi_download1.jpg" + val filePath2 = createFile("multi_download2") + val remotePath2 = "/multi_download2.jpg" + + assertTrue( + UploadFileRemoteOperation(filePath1, remotePath1, "image/jpg", RANDOM_MTIME) + .execute(client) + .isSuccess + ) + assertTrue( + UploadFileRemoteOperation(filePath2, remotePath2, "image/jpg", RANDOM_MTIME) + .execute(client) + .isSuccess + ) + + assertTrue( + DownloadFileRemoteOperation(remotePath1, cacheDir) + .execute(nextcloudClient) + .isSuccess + ) + assertTrue( + DownloadFileRemoteOperation(remotePath2, cacheDir) + .execute(nextcloudClient) + .isSuccess + ) + + val downloaded1 = File(cacheDir + remotePath1) + val downloaded2 = File(cacheDir + remotePath2) + assertTrue(downloaded1.exists()) + assertTrue(downloaded2.exists()) + assertEquals(File(filePath1).length(), downloaded1.length()) + assertEquals(File(filePath2).length(), downloaded2.length()) + } + + @Test + fun downloadAndVerifyContent() { + val filePath = createFile("content_download", 50) + val remotePath = "/content_download.txt" + assertTrue( + UploadFileRemoteOperation(filePath, remotePath, "text/plain", RANDOM_MTIME) + .execute(client) + .isSuccess + ) + + assertTrue( + DownloadFileRemoteOperation(remotePath, cacheDir) + .execute(nextcloudClient) + .isSuccess + ) + + val originalFile = File(filePath) + val downloadedFile = File(cacheDir + remotePath) + assertTrue(downloadedFile.exists()) + assertTrue(originalFile.readBytes().contentEquals(downloadedFile.readBytes())) + } + + @Test + fun downloadedFileExistsAtExpectedPath() { + val filePath = createFile("path_check") + val remotePath = "/path_check.jpg" + assertTrue( + UploadFileRemoteOperation(filePath, remotePath, "image/jpg", RANDOM_MTIME) + .execute(client) + .isSuccess + ) + + assertTrue( + DownloadFileRemoteOperation(remotePath, cacheDir) + .execute(nextcloudClient) + .isSuccess + ) + + val expectedFile = File(cacheDir + remotePath) + assertTrue("Downloaded file should exist at expected path", expectedFile.exists()) + assertTrue("Downloaded file should not be empty", expectedFile.length() >= 0) + } } From f84b5bf9ee6df4a50ed01adb6721affc4ad37e46 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 9 Jun 2026 17:04:49 +0200 Subject: [PATCH 6/6] use ConcurrentHashMap no need for synchronized block Signed-off-by: alperozturk96 --- .../lib/resources/files/DownloadFileRemoteOperation.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.kt index 836fc087c..0500c1c7a 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/files/DownloadFileRemoteOperation.kt @@ -22,6 +22,7 @@ import java.io.IOException import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean @Suppress("NestedBlockDepth", "TooGenericExceptionCaught", "ThrowsCount") @@ -30,7 +31,7 @@ class DownloadFileRemoteOperation( private val remotePath: String, private val temporalFolderPath: String? ) : RemoteOperation() { - private val dataTransferListeners: MutableSet = HashSet() + private val dataTransferListeners = ConcurrentHashMap.newKeySet() private val cancellationRequested = AtomicBoolean(false) var modificationTimestamp: Long = 0 private set @@ -134,11 +135,11 @@ class DownloadFileRemoteOperation( // region public methods fun addProgressListener(listener: OnDatatransferProgressListener) { - synchronized(dataTransferListeners) { dataTransferListeners.add(listener) } + dataTransferListeners.add(listener) } fun removeProgressListener(listener: OnDatatransferProgressListener) { - synchronized(dataTransferListeners) { dataTransferListeners.remove(listener) } + dataTransferListeners.remove(listener) } fun cancel() {