Skip to content

Modification time is not preserved when uploading MediaStore content via Android share #17675

Open

Description

@matthias480

⚠️ Before posting ⚠️

  • This is a bug, not a question or an enhancement.
  • I've searched for similar issues and didn't find a duplicate.
  • I've written a clear and descriptive title for this issue, not just "Bug" or "Crash".
  • I agree to follow Nextcloud's Code of Conduct.

Steps to reproduce

  1. Take an existing media file (photo) whose modification time is older than the current time.
  2. Upload that file normally from within the Nextcloud Android app.
  3. Verify that the original modification time is preserved on the server (as expected).
  4. Delete the uploaded file again or choose another destination.
  5. Open the same file in an Android app with sharing possibility, which shares it as a MediaStore content:// URI, e.g. the Google Photos app.
  6. Use the Share function and select Nextcloud.
  7. Upload the file to Nextcloud.
  8. Compare the modification time of the uploaded file and see that it has the upload time as modification time instead of the original modification time.

Expected behaviour

The original file modification time should be preserved, just as it is when uploading the same file directly from within the Nextcloud app, provided that the source ContentProvider exposes the modification time.

Actual behaviour

When the file is uploaded through Android's Share function, the modification time is not preserved and the uploaded file gets the time of the upload / temporary copy instead.

Android version

17

Device brand and model

Google Pixel 11 Pro

Stock or custom OS?

Stock

Nextcloud android app version

35.0.0

Nextcloud server version

n/a – FileRun server, version 2026.3.0

Using a reverse proxy?

No

Android logs

No response

Server error logs

Additional information

I am using the Nextcloud Android app with a FileRun server, not a Nextcloud Server. However, this issue appears to happen on the Android client side before the actual upload, so the server implementation should not be relevant.

Looking at the current Android app code, CopyAndUploadContentUrisTask.queryLastModified() only checks:

DocumentsContract.Document.COLUMN_LAST_MODIFIED

If that column is not available, the function returns 0L and the modification time is not applied to the temporary file.

This seems problematic for files shared through Android's share sheet using a MediaStore URI such as:

content://media/external/images/media/...

MediaStore commonly exposes the modification timestamp as:

MediaStore.MediaColumns.DATE_MODIFIED

rather than DocumentsContract.Document.COLUMN_LAST_MODIFIED.

This situation can also be seen in the historical issues #7745 and #7752, where a MediaStore cursor exposed date_modified but not last_modified.

A possible solution would be to first try DocumentsContract.Document.COLUMN_LAST_MODIFIED and, if unavailable, fall back to MediaStore.MediaColumns.DATE_MODIFIED.

Please note that the units differ:

  • DocumentsContract.Document.COLUMN_LAST_MODIFIED: milliseconds since epoch
  • MediaStore.MediaColumns.DATE_MODIFIED: seconds since epoch

So the MediaStore value would need to be converted to milliseconds before applying it.

Suggested fix:

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)
        ) {
            return@use TimeUnit.SECONDS.toMillis(
                cursor.getLong(mediaColumn)
            )
        }

        0L
    } ?: 0L
}.getOrDefault(0L)

If neither value is exposed by the provider, the current fallback behaviour could remain unchanged.

Activity

  1. PhilLab commented on Sep 24, 2026

    @PhilLab
    Contributor

    @matthias480 thanks for the report. I'd say feel free to open a PR, and the discussion can go from there.

    ☝️ @alperozturk96

  2. added
    feature: transfers[WIP] Any uploading / downloading matters that aren't auto upload or sync specific
    on Sep 24, 2026
  3. linked a pull request that will close this issuefix(copy-upload): modification date #17704on Sep 24, 2026
  4. alperozturk96 commented on Sep 24, 2026

    @alperozturk96
    Collaborator

    @matthias480 Hello,

    Could you please try with this PR: #17704 ?

  5. matthias480 commented on Sep 25, 2026

    @matthias480
    Author

    @alperozturk96 Hello back,

    thanks for your PR. Unfortunately, the modification time is still not preserved when sharing from Google Photos, so I did some further testing and debugging.

    Google Photos and other apps expose neither last_modified nor date_modified, but I found that ContentResolver.openFileDescriptor(uri, "r") returns a regular file descriptor that can be used to obtain the modification date via fstat().

    So the third generic fallback could be to use fstat() when the provider doesn't expose either timestamp column, but only when the returned descriptor represents a regular file.

    A possible implementation for all three cases could be:

    import android.system.Os
    import android.system.OsConstants
    
    private fun queryLastModified(
        contentResolver: ContentResolver,
        uri: Uri
    ): Long {
        val metadataLastModified = queryLastModifiedFromMetadata(contentResolver, uri)
    
        if (metadataLastModified > 0L) {
            return metadataLastModified
        }
    
        return queryLastModifiedFromFileDescriptor(contentResolver, uri)
    }
    
    private fun queryLastModifiedFromMetadata(
        contentResolver: ContentResolver,
        uri: Uri
    ): Long = runCatching {
        contentResolver.query(uri, null, null, null, null)?.use { cursor ->
            if (!cursor.moveToFirst()) {
                return@use 0L
            }
    
            val timestampColumns = listOf(
                DocumentsContract.Document.COLUMN_LAST_MODIFIED to TimeUnit.MILLISECONDS,
                MediaStore.MediaColumns.DATE_MODIFIED to TimeUnit.SECONDS
            )
    
            for ((columnName, timeUnit) in timestampColumns) {
                val col = cursor.getColumnIndex(columnName)
    
                if (col >= 0 && !cursor.isNull(col)) {
                    val lastModified = cursor.getLong(col)
    
                    if (lastModified > 0L) {
                        return@use TimeUnit.MILLISECONDS.convert(lastModified, timeUnit)
                    }
                }
            }
    
            0L
        } ?: 0L
    }.getOrDefault(0L)
    
    private fun queryLastModifiedFromFileDescriptor(
        contentResolver: ContentResolver,
        uri: Uri
    ): Long = runCatching {
        contentResolver.openFileDescriptor(uri, "r")?.use { parcelFileDescriptor ->
            val stat = Os.fstat(parcelFileDescriptor.fileDescriptor)
    
            if (OsConstants.S_ISREG(stat.st_mode) && stat.st_mtime > 0L) {
                TimeUnit.SECONDS.toMillis(stat.st_mtime)
            } else {
                0L
            }
        } ?: 0L
    }.getOrDefault(0L)

    This would result in the following fallback order:

    COLUMN_LAST_MODIFIED (last_modified) → DATE_MODIFIED (date_modified) → regular file descriptor via fstat() → 0L

    I debugged and tested all three cases successfully:

    • Android file manager → COLUMN_LAST_MODIFIED
    • Fossify Gallery → DATE_MODIFIED
    • Google Photos → fstat() fallback

    All three returned the same and expected original modification time.

  6. alperozturk96 commented on Oct 13, 2026

    @alperozturk96
    Collaborator

    @matthias480 Thank you for the feedback. I updated the PR, you can check :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

3. to reviewbugfeature: transfers[WIP] Any uploading / downloading matters that aren't auto upload or sync specific

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions