mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Merge pull request #7344 from hayaksi1/fix/6371-save-file-name-conflicts
Keep saving files to Downloads once 32 files share the same name
This commit is contained in:
+13
-11
@@ -16,6 +16,7 @@ import android.provider.MediaStore
|
||||
import androidx.annotation.RequiresApi
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.androidutils.file.saveWithUniqueFileName
|
||||
import io.element.android.libraries.androidutils.system.toast
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
@@ -25,6 +26,7 @@ import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
interface FileSave {
|
||||
suspend fun save(
|
||||
@@ -61,18 +63,18 @@ class DefaultFileSave(
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun saveOnDiskUsingMediaStore(path: String) {
|
||||
val file = File(path)
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, file.name)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, MimeTypes.OctetStream)
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
}
|
||||
val resolver = context.contentResolver
|
||||
val outputUri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||
if (outputUri != null) {
|
||||
file.inputStream().use { input ->
|
||||
resolver.openOutputStream(outputUri).use { output ->
|
||||
input.copyTo(output!!, DEFAULT_BUFFER_SIZE)
|
||||
}
|
||||
val outputUri = saveWithUniqueFileName(file.name) { displayName ->
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, MimeTypes.OctetStream)
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
}
|
||||
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||
} ?: throw IOException("Unable to create the destination file")
|
||||
file.inputStream().use { input ->
|
||||
resolver.openOutputStream(outputUri).use { output ->
|
||||
input.copyTo(output!!, DEFAULT_BUFFER_SIZE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.viewfolder.impl.file
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.tests.testutils.fake.registerFakeMediaStoreContentProvider
|
||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import java.io.File
|
||||
|
||||
class DefaultFileSaveTest : RobolectricTest() {
|
||||
@Test
|
||||
fun `save inserts the file in the Downloads collection under its own name`() = runTest {
|
||||
val mediaStore = registerFakeMediaStoreContentProvider()
|
||||
createFileSave().save(aFileToSave().path)
|
||||
assertThat(mediaStore.insertedDisplayNames).containsExactly("a-file.txt")
|
||||
assertThat(mediaStore.savedContent("a-file.txt")).isEqualTo("Some content")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `save retries with a suffixed name when the name cannot be made unique`() = runTest {
|
||||
val mediaStore = registerFakeMediaStoreContentProvider().apply {
|
||||
conflictingDisplayNames = setOf("a-file.txt")
|
||||
}
|
||||
createFileSave().save(aFileToSave().path)
|
||||
assertThat(mediaStore.insertedDisplayNames).hasSize(2)
|
||||
assertThat(mediaStore.insertedDisplayNames.last()).matches("a-file_\\d+\\.txt")
|
||||
assertThat(mediaStore.savedContent(mediaStore.insertedDisplayNames.last())).isEqualTo("Some content")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `save writes nothing when the destination cannot be created`() = runTest {
|
||||
val mediaStore = registerFakeMediaStoreContentProvider().apply {
|
||||
returnsNoUriOnInsert = true
|
||||
}
|
||||
createFileSave().save(aFileToSave().path)
|
||||
assertThat(mediaStore.insertedDisplayNames).containsExactly("a-file.txt")
|
||||
assertThat(mediaStore.savedFileNames()).isEmpty()
|
||||
}
|
||||
|
||||
private fun aFileToSave(): File {
|
||||
return File(RuntimeEnvironment.getApplication().cacheDir, "a-file.txt").apply {
|
||||
writeText("Some content")
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createFileSave() = DefaultFileSave(
|
||||
context = RuntimeEnvironment.getApplication(),
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user