diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt index b48ff98073..35ef6d5d8d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt @@ -131,12 +131,17 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( dismissAfterSend = !useSendQueue, replyParameters = null, ) + + // Clean up the pre-processed media after it's been sent + mediaSender.cleanUp() } } } AttachmentsPreviewEvents.CancelAndDismiss -> { // Cancel media preprocessing and sending preprocessMediaJob?.cancel() + // If we couldn't send the pre-processed media, remove it + mediaSender.cleanUp() ongoingSendAttachmentJob.value?.cancel() // Dismiss the screen diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt index 8b66fd9e62..61c71a233c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt @@ -66,7 +66,7 @@ fun AttachmentsPreviewView( state.eventSink(AttachmentsPreviewEvents.CancelAndClearSendState) } - BackHandler(enabled = state.sendActionState !is SendActionState.Sending) { + BackHandler(enabled = state.sendActionState !is SendActionState.Sending.Uploading) { postCancel() } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt index 31065275b9..1c1ffd45b4 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt @@ -123,8 +123,10 @@ class AttachmentsPreviewPresenterTest { }, ) val onDoneListener = lambdaRecorder { } + val mediaPreProcessor = FakeMediaPreProcessor() val presenter = createAttachmentsPreviewPresenter( room = room, + mediaPreProcessor = mediaPreProcessor, onDoneListener = { onDoneListener() }, ) moleculeFlow(RecompositionMode.Immediate) { @@ -143,6 +145,7 @@ class AttachmentsPreviewPresenterTest { assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done) sendFileResult.assertions().isCalledOnce() onDoneListener.assertions().isCalledOnce() + assertThat(mediaPreProcessor.cleanUpCallCount).isEqualTo(1) } } @@ -159,11 +162,10 @@ class AttachmentsPreviewPresenterTest { ) val onDoneListener = lambdaRecorder { } val processLatch = CompletableDeferred() + val mediaPreProcessor = FakeMediaPreProcessor(processLatch) val presenter = createAttachmentsPreviewPresenter( room = room, - mediaPreProcessor = FakeMediaPreProcessor( - processLatch = processLatch, - ), + mediaPreProcessor = mediaPreProcessor, onDoneListener = { onDoneListener() }, ) moleculeFlow(RecompositionMode.Immediate) { @@ -181,6 +183,7 @@ class AttachmentsPreviewPresenterTest { assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done) sendFileResult.assertions().isCalledOnce() onDoneListener.assertions().isCalledOnce() + assertThat(mediaPreProcessor.cleanUpCallCount).isEqualTo(1) } } @@ -197,11 +200,10 @@ class AttachmentsPreviewPresenterTest { ) val onDoneListener = lambdaRecorder { } val processLatch = CompletableDeferred() + val mediaPreProcessor = FakeMediaPreProcessor(processLatch) val presenter = createAttachmentsPreviewPresenter( room = room, - mediaPreProcessor = FakeMediaPreProcessor( - processLatch = processLatch, - ), + mediaPreProcessor = mediaPreProcessor, onDoneListener = { onDoneListener() }, ) moleculeFlow(RecompositionMode.Immediate) { @@ -219,6 +221,7 @@ class AttachmentsPreviewPresenterTest { assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done) sendFileResult.assertions().isCalledOnce() onDoneListener.assertions().isCalledOnce() + assertThat(mediaPreProcessor.cleanUpCallCount).isEqualTo(1) } } @@ -279,7 +282,9 @@ class AttachmentsPreviewPresenterTest { fun `present - cancel scenario`() = runTest { val onDoneListener = lambdaRecorder { } val deleteCallback = lambdaRecorder {} + val mediaPreProcessor = FakeMediaPreProcessor() val presenter = createAttachmentsPreviewPresenter( + mediaPreProcessor = mediaPreProcessor, temporaryUriDeleter = FakeTemporaryUriDeleter(deleteCallback), onDoneListener = { onDoneListener() }, ) @@ -293,6 +298,7 @@ class AttachmentsPreviewPresenterTest { assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done) deleteCallback.assertions().isCalledOnce() onDoneListener.assertions().isCalledOnce() + assertThat(mediaPreProcessor.cleanUpCallCount).isEqualTo(1) } } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt index da608e08ad..347197bd2d 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt @@ -22,5 +22,10 @@ interface MediaPreProcessor { compressIfPossible: Boolean, ): Result + /** + * Clean up any temporary files or resources used during the media processing. + */ + fun cleanUp() + data class Failure(override val cause: Throwable?) : Exception(cause) } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index 5f542bd91a..80df147dc0 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -200,4 +200,9 @@ class MediaSender @Inject constructor( uploadHandler.await() } } + + /** + * Clean up any temporary files or resources used during the media processing. + */ + fun cleanUp() = preProcessor.cleanUp() } diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt index be0bbc701e..8b93c7e938 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt @@ -41,6 +41,7 @@ import kotlinx.coroutines.withContext import timber.log.Timber import java.io.File import java.io.InputStream +import java.util.UUID import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -68,6 +69,9 @@ class AndroidMediaPreProcessor @Inject constructor( private val contentResolver = context.contentResolver + private val cacheDir = context.cacheDir + private val baseTmpFileDir = File(cacheDir, "uploads") + override suspend fun process( uri: Uri, mimeType: String, @@ -100,6 +104,28 @@ class AndroidMediaPreProcessor @Inject constructor( } }.mapFailure { MediaPreProcessor.Failure(it) } + override fun cleanUp() { + // Clear temporary files created in older versions of the app + cacheDir.listFiles()?.onEach { file -> + if (file.isFile) { + val nameWithoutExtension = file.nameWithoutExtension + // UUIDs are 36 characters long, so we check if we can take those 36 characters + val nameWithoutExtensionAndRandom = if (nameWithoutExtension.length > 36) { + nameWithoutExtension.substring(0, 36) + } else { + // Not a temp file + return@onEach + } + val isUUID = tryOrNull { UUID.fromString(nameWithoutExtensionAndRandom) } != null + if (isUUID && file.extension.isNotEmpty()) { + file.delete() + } + } + } + // Clear temporary files created by this pre-processor in the separate uploads directory + baseTmpFileDir.listFiles()?.onEach { it.delete() } + } + private suspend fun processFile(uri: Uri, mimeType: String): MediaUploadInfo { val file = copyToTmpFile(uri) val info = FileInfo( @@ -280,7 +306,10 @@ class AndroidMediaPreProcessor @Inject constructor( private suspend fun createTmpFileWithInput(inputStream: InputStream): File? { return withContext(coroutineDispatchers.io) { tryOrNull { - val tmpFile = context.createTmpFile() + if (!baseTmpFileDir.exists()) { + baseTmpFileDir.mkdirs() + } + val tmpFile = context.createTmpFile(baseTmpFileDir) tmpFile.outputStream().use { inputStream.copyTo(it) } tmpFile } diff --git a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt index 79f22a4b86..87b739d786 100644 --- a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt +++ b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt @@ -26,6 +26,9 @@ class FakeMediaPreProcessor( var processCallCount = 0 private set + var cleanUpCallCount = 0 + private set + private var result: Result = Result.success( MediaUploadInfo.AnyFile( File("test"), @@ -108,4 +111,8 @@ class FakeMediaPreProcessor( ) ) } + + override fun cleanUp() { + cleanUpCallCount += 1 + } }