From cca43cb25d21d46ebe504e9d50a1356916294b94 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 24 Sep 2026 13:33:45 +0200 Subject: [PATCH 1/3] fix(copy-upload): modification date Signed-off-by: alperozturk96 --- .../ui/asynctasks/CopyAndUploadContentUrisTask.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.kt b/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.kt index 03db1eb5a3fe..0d39c9a232e4 100644 --- a/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.kt @@ -10,6 +10,7 @@ import android.content.ContentResolver import android.content.Context import android.net.Uri import android.provider.DocumentsContract +import android.provider.MediaStore import android.widget.Toast import com.nextcloud.client.account.User import com.nextcloud.client.jobs.upload.FileUploadHelper @@ -26,6 +27,7 @@ import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream import java.lang.ref.WeakReference +import java.util.concurrent.TimeUnit @Suppress("TooGenericExceptionCaught") class CopyAndUploadContentUrisTask( @@ -133,8 +135,15 @@ class CopyAndUploadContentUrisTask( private fun queryLastModified(contentResolver: ContentResolver, uri: Uri): Long = runCatching { contentResolver.query(uri, null, null, null, null)?.use { cursor -> if (!cursor.moveToFirst()) return@use 0L - val col = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED) - if (col >= 0) cursor.getLong(col) else 0L + val documentColumn = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED) + if (documentColumn >= 0 && !cursor.isNull(documentColumn)) return@use cursor.getLong(documentColumn) + + val mediaColumn = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED) + if (mediaColumn >= 0 && !cursor.isNull(mediaColumn)) { + TimeUnit.SECONDS.toMillis(cursor.getLong(mediaColumn)) + } else { + 0L + } } ?: 0L }.getOrDefault(0L) From 97cdbce5b9d340ff34e506aafb5722185c27be48 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 13 Oct 2026 12:03:55 +0200 Subject: [PATCH 2/3] add fallback to OS.fstat via ContentUriLastModifiedResolver Signed-off-by: alperozturk96 --- .../ContentUriLastModifiedResolver.kt | 56 +++++++++++++++++++ .../CopyAndUploadContentUrisTask.kt | 28 ++-------- 2 files changed, 62 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/ui/asynctasks/ContentUriLastModifiedResolver.kt diff --git a/app/src/main/java/com/owncloud/android/ui/asynctasks/ContentUriLastModifiedResolver.kt b/app/src/main/java/com/owncloud/android/ui/asynctasks/ContentUriLastModifiedResolver.kt new file mode 100644 index 000000000000..888b0bd91f0b --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/asynctasks/ContentUriLastModifiedResolver.kt @@ -0,0 +1,56 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.asynctasks + +import android.content.ContentResolver +import android.net.Uri +import android.provider.DocumentsContract +import android.provider.MediaStore +import android.system.Os +import android.system.OsConstants +import android.system.StructStat +import java.util.concurrent.TimeUnit + +class ContentUriLastModifiedResolver(private val contentResolver: ContentResolver, private val uri: Uri) { + companion object { + private val TIMESTAMP_COLUMNS = listOf( + DocumentsContract.Document.COLUMN_LAST_MODIFIED to TimeUnit.MILLISECONDS, + MediaStore.MediaColumns.DATE_MODIFIED to TimeUnit.SECONDS + ) + } + + fun resolve(): Long = fromMetadata().takeIf { it > 0L } ?: fromFileDescriptor() + + private fun fromMetadata(): Long = runCatching { + contentResolver.query(uri, null, null, null, null)?.use { cursor -> + if (!cursor.moveToFirst()) return@use null + + TIMESTAMP_COLUMNS.firstNotNullOfOrNull { (column, unit) -> + runCatching { + cursor.getColumnIndex(column) + .takeIf { it >= 0 && !cursor.isNull(it) } + ?.let(cursor::getLong) + ?.takeIf { it > 0L } + ?.let(unit::toMillis) + }.getOrNull() + } + } + }.getOrNull() ?: 0L + + private fun fromFileDescriptor(): Long = runCatching { + contentResolver.openFileDescriptor(uri, "r")?.use { pfd -> + Os.fstat(pfd.fileDescriptor) + .takeIf { OsConstants.S_ISREG(it.st_mode) } + ?.lastModifiedMillis() + ?.takeIf { it > 0L } + } + }.getOrNull() ?: 0L + + private fun StructStat.lastModifiedMillis(): Long = + TimeUnit.SECONDS.toMillis(st_mtim.tv_sec) + TimeUnit.NANOSECONDS.toMillis(st_mtim.tv_nsec) +} diff --git a/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.kt b/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.kt index 0d39c9a232e4..09544f717d4f 100644 --- a/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.kt @@ -9,8 +9,6 @@ package com.owncloud.android.ui.asynctasks import android.content.ContentResolver import android.content.Context import android.net.Uri -import android.provider.DocumentsContract -import android.provider.MediaStore import android.widget.Toast import com.nextcloud.client.account.User import com.nextcloud.client.jobs.upload.FileUploadHelper @@ -27,7 +25,6 @@ import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream import java.lang.ref.WeakReference -import java.util.concurrent.TimeUnit @Suppress("TooGenericExceptionCaught") class CopyAndUploadContentUrisTask( @@ -72,7 +69,8 @@ class CopyAndUploadContentUrisTask( return try { sourceUris.forEachIndexed { index, uri -> val remotePath = remotePaths[index] - val lastModified = queryLastModified(contentResolver, uri) + val lastModifiedResolver = ContentUriLastModifiedResolver(contentResolver, uri) + val lastModified = lastModifiedResolver.resolve() currentTempPath = "${FileStorageUtils.getTemporalPath(user.accountName)}$remotePath" val cacheFile = File(currentTempPath) @@ -132,25 +130,11 @@ class CopyAndUploadContentUrisTask( } } - private fun queryLastModified(contentResolver: ContentResolver, uri: Uri): Long = runCatching { - contentResolver.query(uri, null, null, null, null)?.use { cursor -> - if (!cursor.moveToFirst()) return@use 0L - val documentColumn = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED) - if (documentColumn >= 0 && !cursor.isNull(documentColumn)) return@use cursor.getLong(documentColumn) - - val mediaColumn = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED) - if (mediaColumn >= 0 && !cursor.isNull(mediaColumn)) { - TimeUnit.SECONDS.toMillis(cursor.getLong(mediaColumn)) - } else { - 0L - } - } ?: 0L - }.getOrDefault(0L) - private fun applyLastModified(file: File, lastModified: Long) { - if (lastModified == 0L) return - runCatching { file.setLastModified(lastModified) } - .onFailure { Log_OC.w(TAG, "Could not set lastModified on cache file: ${it.message}") } + if (lastModified <= 0L) return + if (!file.setLastModified(lastModified)) { + Log_OC.w(TAG, "Could not set lastModified on cache file: ${file.path}") + } } private fun dispatchResult(result: ResultCode) { From ef52e1492f064ea38255d5076d116516c52f20c2 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 13 Oct 2026 12:39:00 +0200 Subject: [PATCH 3/3] improve ContentUriLastModifiedResolver Signed-off-by: alperozturk96 --- .../ui/asynctasks/ContentUriLastModifiedResolver.kt | 8 ++++---- .../android/ui/asynctasks/CopyAndUploadContentUrisTask.kt | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/asynctasks/ContentUriLastModifiedResolver.kt b/app/src/main/java/com/owncloud/android/ui/asynctasks/ContentUriLastModifiedResolver.kt index 888b0bd91f0b..1d4bcf43bf21 100644 --- a/app/src/main/java/com/owncloud/android/ui/asynctasks/ContentUriLastModifiedResolver.kt +++ b/app/src/main/java/com/owncloud/android/ui/asynctasks/ContentUriLastModifiedResolver.kt @@ -16,7 +16,7 @@ import android.system.OsConstants import android.system.StructStat import java.util.concurrent.TimeUnit -class ContentUriLastModifiedResolver(private val contentResolver: ContentResolver, private val uri: Uri) { +class ContentUriLastModifiedResolver(private val contentResolver: ContentResolver) { companion object { private val TIMESTAMP_COLUMNS = listOf( DocumentsContract.Document.COLUMN_LAST_MODIFIED to TimeUnit.MILLISECONDS, @@ -24,9 +24,9 @@ class ContentUriLastModifiedResolver(private val contentResolver: ContentResolve ) } - fun resolve(): Long = fromMetadata().takeIf { it > 0L } ?: fromFileDescriptor() + fun resolve(uri: Uri): Long = fromMetadata(uri).takeIf { it > 0L } ?: fromFileDescriptor(uri) - private fun fromMetadata(): Long = runCatching { + private fun fromMetadata(uri: Uri): Long = runCatching { contentResolver.query(uri, null, null, null, null)?.use { cursor -> if (!cursor.moveToFirst()) return@use null @@ -42,7 +42,7 @@ class ContentUriLastModifiedResolver(private val contentResolver: ContentResolve } }.getOrNull() ?: 0L - private fun fromFileDescriptor(): Long = runCatching { + private fun fromFileDescriptor(uri: Uri): Long = runCatching { contentResolver.openFileDescriptor(uri, "r")?.use { pfd -> Os.fstat(pfd.fileDescriptor) .takeIf { OsConstants.S_ISREG(it.st_mode) } diff --git a/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.kt b/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.kt index 09544f717d4f..b329b35406d7 100644 --- a/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.kt @@ -65,12 +65,12 @@ class CopyAndUploadContentUrisTask( val localPaths = arrayOfNulls(sourceUris.size) val resolvedRemotePaths = arrayOfNulls(sourceUris.size) var currentTempPath: String? = null + val lastModifiedResolver = ContentUriLastModifiedResolver(contentResolver) return try { sourceUris.forEachIndexed { index, uri -> val remotePath = remotePaths[index] - val lastModifiedResolver = ContentUriLastModifiedResolver(contentResolver, uri) - val lastModified = lastModifiedResolver.resolve() + val lastModified = lastModifiedResolver.resolve(uri) currentTempPath = "${FileStorageUtils.getTemporalPath(user.accountName)}$remotePath" val cacheFile = File(currentTempPath)