API modules do not need to setup dependency injection.

Move the implementation to the impl modules.
This commit is contained in:
Benoit Marty 2025-11-12 16:03:00 +01:00 committed by Benoit Marty
parent 9eb66afd87
commit b3ec256dfa
43 changed files with 620 additions and 405 deletions

View file

@ -1,4 +1,3 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
@ -13,8 +12,6 @@ plugins {
id("io.element.android-library")
}
setupDependencyInjection()
android {
namespace = "io.element.android.libraries.mediaupload.api"
}
@ -27,9 +24,4 @@ dependencies {
api(projects.libraries.matrix.api)
api(projects.libraries.preferences.api)
implementation(libs.coroutines.core)
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.mediaupload.test)
}

View file

@ -8,17 +8,9 @@
package io.element.android.libraries.mediaupload.api
import dev.zacsweers.metro.Inject
import io.element.android.libraries.matrix.api.MatrixClient
/**
* Provides the maximum upload size allowed by the Matrix server.
*/
@Inject
class MaxUploadSizeProvider(
private val matrixClient: MatrixClient,
) {
suspend fun getMaxUploadSize(): Result<Long> {
return matrixClient.getMaxFileUploadSize()
}
fun interface MaxUploadSizeProvider {
suspend fun getMaxUploadSize(): Result<Long>
}

View file

@ -9,73 +9,41 @@
package io.element.android.libraries.mediaupload.api
import android.net.Uri
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.core.extensions.flatMapCatching
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import timber.log.Timber
import java.io.File
import java.util.concurrent.ConcurrentHashMap
@AssistedInject
class MediaSender(
private val preProcessor: MediaPreProcessor,
private val room: JoinedRoom,
@Assisted private val timelineMode: Timeline.Mode,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
) {
@AssistedFactory
interface Factory {
fun create(
timelineMode: Timeline.Mode,
): MediaSender
}
fun interface MediaSenderFactory {
/**
* Create a [MediaSender] for the given [Timeline.Mode], in the Room Scope.
*/
fun create(
timelineMode: Timeline.Mode,
): MediaSender
}
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
fun interface MediaSenderRoomFactory {
/**
* Create a [MediaSender] for the given [JoinedRoom], with timeline mode Live.
*/
fun create(
room: JoinedRoom,
): MediaSender
}
interface MediaSender {
suspend fun preProcessMedia(
uri: Uri,
mimeType: String,
mediaOptimizationConfig: MediaOptimizationConfig,
): Result<MediaUploadInfo> {
Timber.d("Pre-processing media | uri: ${mediaId(uri)} | mimeType: $mimeType")
return preProcessor
.process(
uri = uri,
mimeType = mimeType,
deleteOriginal = false,
mediaOptimizationConfig = mediaOptimizationConfig,
)
}
): Result<MediaUploadInfo>
suspend fun sendPreProcessedMedia(
mediaUploadInfo: MediaUploadInfo,
caption: String?,
formattedCaption: String?,
inReplyToEventId: EventId?,
): Result<Unit> {
val mediaLogId = mediaId(mediaUploadInfo.file)
return getTimeline().flatMap {
Timber.d("Started sending media $mediaLogId using timeline: ${it.mode}")
it.sendMedia(
uploadInfo = mediaUploadInfo,
caption = caption,
formattedCaption = formattedCaption,
inReplyToEventId = inReplyToEventId,
)
}
.handleSendResult(mediaLogId)
}
): Result<Unit>
suspend fun sendMedia(
uri: Uri,
@ -84,147 +52,14 @@ class MediaSender(
formattedCaption: String? = null,
inReplyToEventId: EventId? = null,
mediaOptimizationConfig: MediaOptimizationConfig,
): Result<Unit> {
return preProcessor
.process(
uri = uri,
mimeType = mimeType,
deleteOriginal = false,
mediaOptimizationConfig = mediaOptimizationConfig,
)
.flatMapCatching { info ->
getTimeline().getOrThrow().sendMedia(
uploadInfo = info,
caption = caption,
formattedCaption = formattedCaption,
inReplyToEventId = inReplyToEventId,
)
}
.handleSendResult(mediaId(uri))
}
): Result<Unit>
suspend fun sendVoiceMessage(
uri: Uri,
mimeType: String,
waveForm: List<Float>,
inReplyToEventId: EventId? = null,
): Result<Unit> {
return preProcessor
.process(
uri = uri,
mimeType = mimeType,
deleteOriginal = true,
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
)
.flatMapCatching { info ->
val audioInfo = (info as MediaUploadInfo.Audio).audioInfo
val newInfo = MediaUploadInfo.VoiceMessage(
file = info.file,
audioInfo = audioInfo,
waveform = waveForm,
)
getTimeline().getOrThrow().sendMedia(
uploadInfo = newInfo,
caption = null,
formattedCaption = null,
inReplyToEventId = inReplyToEventId,
)
}
.handleSendResult(mediaId(uri))
}
): Result<Unit>
private fun Result<Unit>.handleSendResult(mediaId: String) = this
.onFailure { error ->
val job = ongoingUploadJobs.remove(Job)
Timber.e(error, "Sending media $mediaId failed. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}")
if (error !is CancellationException) {
job?.cancel()
}
}
.onSuccess {
Timber.d("Sent media $mediaId successfully. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}")
ongoingUploadJobs.remove(Job)
}
private suspend fun Timeline.sendMedia(
uploadInfo: MediaUploadInfo,
caption: String?,
formattedCaption: String?,
inReplyToEventId: EventId?,
): Result<Unit> {
val handler = when (uploadInfo) {
is MediaUploadInfo.Image -> {
sendImage(
file = uploadInfo.file,
thumbnailFile = uploadInfo.thumbnailFile,
imageInfo = uploadInfo.imageInfo,
caption = caption,
formattedCaption = formattedCaption,
inReplyToEventId = inReplyToEventId,
)
}
is MediaUploadInfo.Video -> {
sendVideo(
file = uploadInfo.file,
thumbnailFile = uploadInfo.thumbnailFile,
videoInfo = uploadInfo.videoInfo,
caption = caption,
formattedCaption = formattedCaption,
inReplyToEventId = inReplyToEventId,
)
}
is MediaUploadInfo.Audio -> {
sendAudio(
file = uploadInfo.file,
audioInfo = uploadInfo.audioInfo,
caption = caption,
formattedCaption = formattedCaption,
inReplyToEventId = inReplyToEventId,
)
}
is MediaUploadInfo.VoiceMessage -> {
sendVoiceMessage(
file = uploadInfo.file,
audioInfo = uploadInfo.audioInfo,
waveform = uploadInfo.waveform,
inReplyToEventId = inReplyToEventId,
)
}
is MediaUploadInfo.AnyFile -> {
sendFile(
file = uploadInfo.file,
fileInfo = uploadInfo.fileInfo,
caption = caption,
formattedCaption = formattedCaption,
inReplyToEventId = inReplyToEventId,
)
}
}
// We handle the cancellations here manually, so we suppress the warning
@Suppress("RunCatchingNotAllowed")
return handler
.mapCatching { uploadHandler ->
Timber.d("Added ongoing upload job, total: ${ongoingUploadJobs.size + 1}")
ongoingUploadJobs[Job] = uploadHandler
uploadHandler.await()
}
}
private suspend fun getTimeline(): Result<Timeline> {
return when (timelineMode) {
is Timeline.Mode.Thread -> {
room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = timelineMode.threadRootId))
}
else -> Result.success(room.liveTimeline)
}
}
/**
* Clean up any temporary files or resources used during the media processing.
*/
fun cleanUp() = preProcessor.cleanUp()
fun cleanUp()
}
private fun mediaId(uri: Uri?): String = uri?.path.orEmpty().hash()
private fun mediaId(file: File): String = file.path.orEmpty().hash()

View file

@ -42,4 +42,7 @@ dependencies {
testCommonDependencies(libs)
testImplementation(projects.services.toolbox.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.mediaupload.test)
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector 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.libraries.mediaupload.impl
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.mediaupload.api.MaxUploadSizeProvider
/**
* Provides the maximum upload size allowed by the Matrix server.
*/
@ContributesBinding(SessionScope::class)
class DefaultMaxUploadSizeProvider(
private val matrixClient: MatrixClient,
) : MaxUploadSizeProvider {
override suspend fun getMaxUploadSize(): Result<Long> {
return matrixClient.getMaxFileUploadSize()
}
}

View file

@ -0,0 +1,264 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.libraries.mediaupload.impl
import android.net.Uri
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.core.extensions.flatMapCatching
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.api.MediaSenderFactory
import io.element.android.libraries.mediaupload.api.MediaSenderRoomFactory
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import timber.log.Timber
import java.io.File
import java.util.concurrent.ConcurrentHashMap
@ContributesBinding(RoomScope::class)
class DefaultMediaSenderFactory(
private val preProcessor: MediaPreProcessor,
private val room: JoinedRoom,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
) : MediaSenderFactory {
override fun create(
timelineMode: Timeline.Mode,
): MediaSender {
return DefaultMediaSender(
preProcessor = preProcessor,
room = room,
timelineMode = timelineMode,
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
)
}
}
@ContributesBinding(SessionScope::class)
class DefaultMediaSenderRoomFactory(
private val preProcessor: MediaPreProcessor,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
) : MediaSenderRoomFactory {
override fun create(
room: JoinedRoom,
): MediaSender {
return DefaultMediaSender(
preProcessor = preProcessor,
room = room,
timelineMode = Timeline.Mode.Live,
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
)
}
}
class DefaultMediaSender(
private val preProcessor: MediaPreProcessor,
private val room: JoinedRoom,
private val timelineMode: Timeline.Mode,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
) : MediaSender {
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
override suspend fun preProcessMedia(
uri: Uri,
mimeType: String,
mediaOptimizationConfig: MediaOptimizationConfig,
): Result<MediaUploadInfo> {
Timber.d("Pre-processing media | uri: ${mediaId(uri)} | mimeType: $mimeType")
return preProcessor
.process(
uri = uri,
mimeType = mimeType,
deleteOriginal = false,
mediaOptimizationConfig = mediaOptimizationConfig,
)
}
override suspend fun sendPreProcessedMedia(
mediaUploadInfo: MediaUploadInfo,
caption: String?,
formattedCaption: String?,
inReplyToEventId: EventId?,
): Result<Unit> {
val mediaLogId = mediaId(mediaUploadInfo.file)
return getTimeline().flatMap {
Timber.d("Started sending media $mediaLogId using timeline: ${it.mode}")
it.sendMedia(
uploadInfo = mediaUploadInfo,
caption = caption,
formattedCaption = formattedCaption,
inReplyToEventId = inReplyToEventId,
)
}
.handleSendResult(mediaLogId)
}
override suspend fun sendMedia(
uri: Uri,
mimeType: String,
caption: String?,
formattedCaption: String?,
inReplyToEventId: EventId?,
mediaOptimizationConfig: MediaOptimizationConfig,
): Result<Unit> {
return preProcessor
.process(
uri = uri,
mimeType = mimeType,
deleteOriginal = false,
mediaOptimizationConfig = mediaOptimizationConfig,
)
.flatMapCatching { info ->
getTimeline().getOrThrow().sendMedia(
uploadInfo = info,
caption = caption,
formattedCaption = formattedCaption,
inReplyToEventId = inReplyToEventId,
)
}
.handleSendResult(mediaId(uri))
}
override suspend fun sendVoiceMessage(
uri: Uri,
mimeType: String,
waveForm: List<Float>,
inReplyToEventId: EventId?,
): Result<Unit> {
return preProcessor
.process(
uri = uri,
mimeType = mimeType,
deleteOriginal = true,
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
)
.flatMapCatching { info ->
val audioInfo = (info as MediaUploadInfo.Audio).audioInfo
val newInfo = MediaUploadInfo.VoiceMessage(
file = info.file,
audioInfo = audioInfo,
waveform = waveForm,
)
getTimeline().getOrThrow().sendMedia(
uploadInfo = newInfo,
caption = null,
formattedCaption = null,
inReplyToEventId = inReplyToEventId,
)
}
.handleSendResult(mediaId(uri))
}
private fun Result<Unit>.handleSendResult(mediaId: String) = this
.onFailure { error ->
val job = ongoingUploadJobs.remove(Job)
Timber.e(error, "Sending media $mediaId failed. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}")
if (error !is CancellationException) {
job?.cancel()
}
}
.onSuccess {
Timber.d("Sent media $mediaId successfully. Removing ongoing upload job. Total: ${ongoingUploadJobs.size}")
ongoingUploadJobs.remove(Job)
}
private suspend fun Timeline.sendMedia(
uploadInfo: MediaUploadInfo,
caption: String?,
formattedCaption: String?,
inReplyToEventId: EventId?,
): Result<Unit> {
val handler = when (uploadInfo) {
is MediaUploadInfo.Image -> {
sendImage(
file = uploadInfo.file,
thumbnailFile = uploadInfo.thumbnailFile,
imageInfo = uploadInfo.imageInfo,
caption = caption,
formattedCaption = formattedCaption,
inReplyToEventId = inReplyToEventId,
)
}
is MediaUploadInfo.Video -> {
sendVideo(
file = uploadInfo.file,
thumbnailFile = uploadInfo.thumbnailFile,
videoInfo = uploadInfo.videoInfo,
caption = caption,
formattedCaption = formattedCaption,
inReplyToEventId = inReplyToEventId,
)
}
is MediaUploadInfo.Audio -> {
sendAudio(
file = uploadInfo.file,
audioInfo = uploadInfo.audioInfo,
caption = caption,
formattedCaption = formattedCaption,
inReplyToEventId = inReplyToEventId,
)
}
is MediaUploadInfo.VoiceMessage -> {
sendVoiceMessage(
file = uploadInfo.file,
audioInfo = uploadInfo.audioInfo,
waveform = uploadInfo.waveform,
inReplyToEventId = inReplyToEventId,
)
}
is MediaUploadInfo.AnyFile -> {
sendFile(
file = uploadInfo.file,
fileInfo = uploadInfo.fileInfo,
caption = caption,
formattedCaption = formattedCaption,
inReplyToEventId = inReplyToEventId,
)
}
}
// We handle the cancellations here manually, so we suppress the warning
@Suppress("RunCatchingNotAllowed")
return handler
.mapCatching { uploadHandler ->
Timber.d("Added ongoing upload job, total: ${ongoingUploadJobs.size + 1}")
ongoingUploadJobs[Job] = uploadHandler
uploadHandler.await()
}
}
private suspend fun getTimeline(): Result<Timeline> {
return when (timelineMode) {
is Timeline.Mode.Thread -> {
room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = timelineMode.threadRootId))
}
else -> Result.success(room.liveTimeline)
}
}
/**
* Clean up any temporary files or resources used during the media processing.
*/
override fun cleanUp() = preProcessor.cleanUp()
}
private fun mediaId(uri: Uri?): String = uri?.path.orEmpty().hash()
private fun mediaId(file: File): String = file.path.orEmpty().hash()

View file

@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.mediaupload.api
package io.element.android.libraries.mediaupload.impl
import android.net.Uri
import com.google.common.truth.Truth.assertThat
@ -19,6 +19,9 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -33,7 +36,7 @@ import org.robolectric.RobolectricTestRunner
import java.io.File
@RunWith(RobolectricTestRunner::class)
class MediaSenderTest {
class DefaultMediaSenderTest {
private val mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
@ -42,7 +45,7 @@ class MediaSenderTest {
@Test
fun `given an attachment when sending it the preprocessor always runs`() = runTest {
val preProcessor = FakeMediaPreProcessor()
val sender = createMediaSender(
val sender = createDefaultMediaSender(
preProcessor = preProcessor,
room = FakeJoinedRoom(
liveTimeline = FakeTimeline().apply {
@ -77,7 +80,7 @@ class MediaSenderTest {
sendImageLambda = sendImageResult
},
)
val sender = createMediaSender(room = room)
val sender = createDefaultMediaSender(room = room)
val uri = Uri.parse("content://image.jpg")
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
@ -88,7 +91,7 @@ class MediaSenderTest {
val preProcessor = FakeMediaPreProcessor().apply {
givenResult(Result.failure(Exception()))
}
val sender = createMediaSender(preProcessor)
val sender = createDefaultMediaSender(preProcessor)
val uri = Uri.parse("content://image.jpg")
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
@ -110,7 +113,7 @@ class MediaSenderTest {
sendImageLambda = sendImageResult
},
)
val sender = createMediaSender(
val sender = createDefaultMediaSender(
preProcessor = preProcessor,
room = room,
)
@ -133,7 +136,7 @@ class MediaSenderTest {
sendFileLambda = sendFileResult
},
)
val sender = createMediaSender(room = room)
val sender = createDefaultMediaSender(room = room)
val sendJob = launch {
val uri = Uri.parse("content://image.jpg")
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, mediaOptimizationConfig = mediaOptimizationConfig)
@ -155,11 +158,11 @@ class MediaSenderTest {
sendFileResult.assertions().isCalledOnce()
}
private fun createMediaSender(
private fun createDefaultMediaSender(
preProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
room: JoinedRoom = FakeJoinedRoom(),
mediaOptimizationConfigProvider: MediaOptimizationConfigProvider = MediaOptimizationConfigProvider { mediaOptimizationConfig },
) = MediaSender(
) = DefaultMediaSender(
preProcessor = preProcessor,
room = room,
timelineMode = Timeline.Mode.Live,

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2025 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.libraries.mediaupload.test
import android.net.Uri
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
import io.element.android.tests.testutils.lambda.lambdaError
class FakeMediaSender(
private val preProcessMediaResult: () -> Result<MediaUploadInfo> = { lambdaError() },
private val sendPreProcessedMediaResult: () -> Result<Unit> = { lambdaError() },
private val sendMediaResult: () -> Result<Unit> = { lambdaError() },
private val sendVoiceMessageResult: () -> Result<Unit> = { lambdaError() },
private val cleanUpResult: () -> Unit = { lambdaError() },
) : MediaSender {
override suspend fun preProcessMedia(
uri: Uri,
mimeType: String,
mediaOptimizationConfig: MediaOptimizationConfig,
): Result<MediaUploadInfo> {
return preProcessMediaResult()
}
override suspend fun sendPreProcessedMedia(
mediaUploadInfo: MediaUploadInfo,
caption: String?,
formattedCaption: String?,
inReplyToEventId: EventId?,
): Result<Unit> {
return sendPreProcessedMediaResult()
}
override suspend fun sendMedia(
uri: Uri,
mimeType: String,
caption: String?,
formattedCaption: String?,
inReplyToEventId: EventId?,
mediaOptimizationConfig: MediaOptimizationConfig,
): Result<Unit> {
return sendMediaResult()
}
override suspend fun sendVoiceMessage(
uri: Uri,
mimeType: String,
waveForm: List<Float>,
inReplyToEventId: EventId?,
): Result<Unit> {
return sendVoiceMessageResult()
}
override fun cleanUp() {
cleanUpResult()
}
}