Skip to content

Move images by updating RELATIVE_PATH #2144

Description

@flexagoon

Guidelines

Use case

Currently, Aves moves images by writing the source bytes to the new file stream. This works, but takes a long time when moving a lot of images. When using the "preserve dates" option, which currently works by modifying Exif metadata, it takes even longer.

Proposal

Android apparently provides a way to update the RELATIVE_PATH field of a media file directly, which moves it to the new location instantly and doesn't change the modification date.

I am not very knowledgeable about this, but here is the AI output from analyzing https://github.com/IacobIonut01/ReFra which uses this method, which could provide some guidance:

Slop explanation Here's how it works:

The trick: using MediaStore.RELATIVE_PATH instead of copy+delete

The critical code is in MediaRepositoryImpl.kt:

override suspend fun <T : Media> moveMedia(
    media: T,
    newPath: String
): Boolean {
    val (destVolume, destRelPath) = resolveMediaStoreVolume(newPath)
    val sourceVolume = media.mediaStoreVolumeName

    if (destVolume == sourceVolume) {
        return context.updateMedia(
            media = media,
            contentValues = relativePath(destRelPath)
        )
    }

    return crossVolumeMove(media, destVolume, destRelPath)
}

The relativePath helper (same file, bottom) just creates:

private fun relativePath(newPath: String) = ContentValues().apply {
    put(MediaStore.MediaColumns.RELATIVE_PATH, newPath)
}

This is the key. On Android 10+ (API 29+, scoped storage), RELATIVE_PATH is a virtual column in MediaStore. When you call ContentResolver.update() with just RELATIVE_PATH set to a new directory, Android itself moves the file on disk — without ever opening, reading, or writing the file's contents. The file's mtime (last-modified timestamp) is left untouched because it's just a filesystem-level rename/move, not a copy.

Contrast this with naive gallery apps that do a manual "read source → write to destination → delete source" pipeline. That creates a brand new file at the destination, which gets the current time as its modification date.

The cross-volume exception

For moves between different storage volumes (e.g., internal storage → SD card), the app does fall back to a copy+delete in crossVolumeMove(), and it explicitly sets DATE_MODIFIED to current time there. This is unavoidable — you can't do a zero-cost rename across volumes. But for the common case of moving photos between albums on the same storage device, the RELATIVE_PATH trick preserves the original date perfectly.


Alternatives

Changing the modification date via Exif does have major advantages over this method, since it also preserves the date during file synchronization, etc.

However, even if you want to keep the Exif method of automatically preserving dates, using this API would still make the file moving much faster and would greatly improve the user experience

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions