Keep saving files to Downloads once 32 files share the same name

MediaStore de-duplicates the display name of a new file itself, but its
uniquifier gives up after 32 collisions and throws IllegalStateException.
Saving then failed with "Sorry, an error occurred" and wrote nothing, and
because image.png is the name most screenshots arrive with, a user who
saves media regularly could reach a state where that name never saved
again without deleting files from Downloads by hand.

A new saveWithUniqueFileName helper in androidutils retries the insert
once with a suffixed name when the platform reports it cannot build a
unique one; a second failure propagates so genuine errors still surface.
withFileNameSuffix builds that name by inserting the suffix before the
extension, and handles names without one.

Both call sites are fixed: AndroidLocalMediaActions and DefaultFileSave
carried byte-identical logic and therefore the identical defect. While in
AndroidLocalMediaActions, a null output uri from ContentResolver.insert
now throws instead of reporting success without writing anything.

The pre-Android-Q path is untouched: it overwrites rather than
uniquifying, so it never reaches this limit.

Relates to https://github.com/element-hq/element-x-android/issues/6371
This commit is contained in:
hayaksi1
2026-08-09 01:59:17 +03:00
parent 044737a3a7
commit 17365a50d1
4 changed files with 150 additions and 22 deletions
@@ -42,6 +42,41 @@ fun File.safeRenameTo(dest: File) {
)
}
/**
* Returns this file name with [suffix] inserted before its extension, i.e. `"image.png"` with the
* suffix `"_1"` gives `"image_1.png"`. A name without extension simply gets the suffix appended.
*/
fun String.withFileNameSuffix(suffix: String): String {
val extension = substringAfterLast('.', "")
return if (extension.isEmpty()) {
"$this$suffix"
} else {
"${substringBeforeLast('.')}$suffix.$extension"
}
}
/**
* Runs [saveAs] with [fileName], and runs it once more with a suffixed name if the platform failed to
* build a unique name for it.
*
* `MediaStore` de-duplicates the display name of a new file itself, but gives up after 32 conflicts
* and throws `IllegalStateException("Failed to build unique file")`, which makes saving a file with a
* common name such as `image.png` fail for good.
* See https://github.com/element-hq/element-x-android/issues/6371
*/
fun <T> saveWithUniqueFileName(
fileName: String,
uniqueSuffix: () -> String = { "_${System.currentTimeMillis()}" },
saveAs: (String) -> T,
): T {
return try {
saveAs(fileName)
} catch (nameConflict: IllegalStateException) {
Timber.w(nameConflict, "Unable to build a unique file name, retrying with a suffixed one")
saveAs(fileName.withFileNameSuffix(uniqueSuffix()))
}
}
fun Context.createTmpFile(baseDir: File = cacheDir, extension: String? = null): File {
val suffix = extension?.let { ".$extension" }
return File.createTempFile(UUID.randomUUID().toString(), suffix, baseDir).apply { mkdirs() }