From 6b1abd6564fbd4e1a2d3d489f9d9bba58280f938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Tue, 25 Jun 2024 14:05:52 +0200 Subject: [PATCH] fix: audio converting time [WPB-9705] (#3127) --- .../sendmessage/SendMessageViewModel.kt | 4 +- .../recordaudio/AudioMediaRecorder.kt | 221 +++++++++++++----- .../GenerateAudioFileWithEffectsUseCase.kt | 33 ++- .../recordaudio/RecordAudioButtons.kt | 74 ++++-- .../recordaudio/RecordAudioComponent.kt | 5 + .../recordaudio/RecordAudioState.kt | 7 +- .../recordaudio/RecordAudioViewModel.kt | 17 +- .../kotlin/com/wire/android/util/FileUtil.kt | 2 +- app/src/main/res/values/strings.xml | 1 + .../recordaudio/RecordAudioViewModelTest.kt | 22 +- 10 files changed, 277 insertions(+), 109 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt index a3301d5e8e2..60656a9ed52 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt @@ -40,7 +40,7 @@ import com.wire.android.ui.home.messagecomposer.model.MessageBundle import com.wire.android.ui.home.messagecomposer.model.Ping import com.wire.android.ui.navArgs import com.wire.android.ui.sharing.SendMessagesSnackbarMessages -import com.wire.android.util.AUDIO_MIME_TYPE +import com.wire.android.util.SUPPORTED_AUDIO_MIME_TYPE import com.wire.android.util.ImageUtil import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.getAudioLengthInMs @@ -209,7 +209,7 @@ class SendMessageViewModel @Inject constructor( handleAssetMessageBundle( attachmentUri = messageBundle.attachmentUri, conversationId = messageBundle.conversationId, - specifiedMimeType = AUDIO_MIME_TYPE, + specifiedMimeType = SUPPORTED_AUDIO_MIME_TYPE, ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt index 5cdbfe8ddda..dc327c31fdc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt @@ -17,13 +17,15 @@ */ package com.wire.android.ui.home.messagecomposer.recordaudio -import android.content.Context +import android.annotation.SuppressLint +import android.media.AudioFormat +import android.media.AudioRecord import android.media.MediaRecorder -import android.os.Build import com.wire.android.appLogger -import com.wire.android.util.fileDateTime import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.android.util.fileDateTime import com.wire.kalium.logic.data.asset.KaliumFileSystem +import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCaseImpl.Companion.ASSET_SIZE_DEFAULT_LIMIT_BYTES import com.wire.kalium.util.DateTimeUtil import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.CoroutineScope @@ -32,13 +34,15 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch -import java.io.File +import okio.Path +import okio.buffer import java.io.IOException +import java.nio.ByteBuffer +import java.nio.ByteOrder import javax.inject.Inject @ViewModelScoped class AudioMediaRecorder @Inject constructor( - private val context: Context, private val kaliumFileSystem: KaliumFileSystem, private val dispatcherProvider: DispatcherProvider ) { @@ -47,88 +51,187 @@ class AudioMediaRecorder @Inject constructor( CoroutineScope(SupervisorJob() + dispatcherProvider.io()) } - private var mediaRecorder: MediaRecorder? = null + private var audioRecorder: AudioRecord? = null + private var recordingThread: Thread? = null + private var isRecording = false + private var assetLimitInMB: Long = ASSET_SIZE_DEFAULT_LIMIT_BYTES - var originalOutputFile: File? = null - var effectsOutputFile: File? = null + var originalOutputPath: Path? = null + var effectsOutputPath: Path? = null private val _maxFileSizeReached = MutableSharedFlow() fun getMaxFileSizeReached(): Flow = _maxFileSizeReached.asSharedFlow() + @SuppressLint("MissingPermission") fun setUp(assetLimitInMegabyte: Long) { - if (mediaRecorder == null) { - mediaRecorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - MediaRecorder(context) - } else { - MediaRecorder() - } - - originalOutputFile = kaliumFileSystem + assetLimitInMB = assetLimitInMegabyte + if (audioRecorder == null) { + val bufferSize = AudioRecord.getMinBufferSize( + SAMPLING_RATE, + AUDIO_CHANNELS, + AUDIO_ENCODING + ) + + audioRecorder = AudioRecord( + MediaRecorder.AudioSource.MIC, + SAMPLING_RATE, + AUDIO_CHANNELS, + AUDIO_ENCODING, + bufferSize + ) + + originalOutputPath = kaliumFileSystem .tempFilePath(getRecordingAudioFileName()) - .toFile() - effectsOutputFile = kaliumFileSystem + effectsOutputPath = kaliumFileSystem .tempFilePath(getRecordingAudioEffectsFileName()) - .toFile() - - mediaRecorder?.setAudioSource(MediaRecorder.AudioSource.MIC) - mediaRecorder?.setAudioSamplingRate(SAMPLING_RATE) - mediaRecorder?.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) - mediaRecorder?.setAudioEncoder(MediaRecorder.AudioEncoder.AAC) - mediaRecorder?.setAudioChannels(AUDIO_CHANNELS) - mediaRecorder?.setAudioEncodingBitRate(AUDIO_ENCONDING_BIT_RATE) - mediaRecorder?.setMaxFileSize(assetLimitInMegabyte) - mediaRecorder?.setOutputFile(originalOutputFile) - - observeAudioFileSize(assetLimitInMegabyte) } } fun startRecording(): Boolean = try { - mediaRecorder?.prepare() - mediaRecorder?.start() - true - } catch (e: IllegalStateException) { - e.printStackTrace() - appLogger.e("[RecordAudio] startRecording: IllegalStateException - ${e.message}") - false - } catch (e: IOException) { - e.printStackTrace() - appLogger.e("[RecordAudio] startRecording: IOException - ${e.message}") - false - } + audioRecorder?.startRecording() + isRecording = true + recordingThread = Thread { writeAudioDataToFile() } + recordingThread?.start() + true + } catch (e: IllegalStateException) { + e.printStackTrace() + appLogger.e("[RecordAudio] startRecording: IllegalStateException - ${e.message}") + false + } catch (e: IOException) { + e.printStackTrace() + appLogger.e("[RecordAudio] startRecording: IOException - ${e.message}") + false + } + + private fun writeWavHeader(bufferedSink: okio.BufferedSink, sampleRate: Int, channels: Int, bitsPerSample: Int) { + val byteRate = sampleRate * channels * (bitsPerSample / BITS_PER_BYTE) + val blockAlign = channels * (bitsPerSample / BITS_PER_BYTE) + + // We use buffer() to correctly write the string values. + bufferedSink.writeUtf8(CHUNK_ID_RIFF) // Chunk ID + bufferedSink.writeIntLe(PLACEHOLDER_SIZE) // Placeholder for Chunk Size (will be updated later) + bufferedSink.writeUtf8(FORMAT_WAVE) // Format + bufferedSink.writeUtf8(SUBCHUNK1_ID_FMT) // Subchunk1 ID + bufferedSink.writeIntLe(SUBCHUNK1_SIZE_PCM) // Subchunk1 Size (PCM) + bufferedSink.writeShortLe(AUDIO_FORMAT_PCM) // Audio Format (PCM) + bufferedSink.writeShortLe(channels) // Number of Channels + bufferedSink.writeIntLe(sampleRate) // Sample Rate + bufferedSink.writeIntLe(byteRate) // Byte Rate + bufferedSink.writeShortLe(blockAlign) // Block Align + bufferedSink.writeShortLe(bitsPerSample) // Bits Per Sample + bufferedSink.writeUtf8(SUBCHUNK2_ID_DATA) // Subchunk2 ID + bufferedSink.writeIntLe(PLACEHOLDER_SIZE) // Placeholder for Subchunk2 Size (will be updated later) + } + + private fun updateWavHeader(filePath: Path) { + val file = filePath.toFile() + val fileSize = file.length().toInt() + val dataSize = fileSize - HEADER_SIZE + + val chunkSizeBuffer = ByteBuffer.allocate(INT_SIZE) + chunkSizeBuffer.order(ByteOrder.LITTLE_ENDIAN) + chunkSizeBuffer.putInt(fileSize - CHUNK_ID_SIZE) + + val dataSizeBuffer = ByteBuffer.allocate(INT_SIZE) + dataSizeBuffer.order(ByteOrder.LITTLE_ENDIAN) + dataSizeBuffer.putInt(dataSize) + + val randomAccessFile = java.io.RandomAccessFile(file, "rw") + + // Update Chunk Size + randomAccessFile.seek(CHUNK_SIZE_OFFSET.toLong()) + randomAccessFile.write(chunkSizeBuffer.array()) + + // Update Subchunk2 Size + randomAccessFile.seek(SUBCHUNK2_SIZE_OFFSET.toLong()) + randomAccessFile.write(dataSizeBuffer.array()) + + randomAccessFile.close() + + appLogger.i("Updated WAV Header: Chunk Size = ${fileSize - CHUNK_ID_SIZE}, Data Size = $dataSize") + } fun stop() { - mediaRecorder?.stop() + isRecording = false + audioRecorder?.stop() + recordingThread?.join() } fun release() { - mediaRecorder?.release() + audioRecorder?.release() + audioRecorder = null } - private fun observeAudioFileSize(assetLimitInMegabyte: Long) { - mediaRecorder?.setOnInfoListener { _, what, _ -> - if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { - scope.launch { - _maxFileSizeReached.emit( - RecordAudioDialogState.MaxFileSizeReached( - maxSize = assetLimitInMegabyte.div(SIZE_OF_1MB) + private fun writeAudioDataToFile() { + val data = ByteArray(BUFFER_SIZE) + var sink: okio.Sink? = null + + try { + sink = kaliumFileSystem.sink(originalOutputPath!!) + val bufferedSink = sink.buffer() + + // Write WAV header + writeWavHeader(bufferedSink, SAMPLING_RATE, AUDIO_CHANNELS, BITS_PER_SAMPLE) + + while (isRecording) { + val read = audioRecorder?.read(data, 0, BUFFER_SIZE) ?: 0 + if (read > 0) { + bufferedSink.write(data, 0, read) + } + + // Check if the file size exceeds the limit + val currentSize = originalOutputPath!!.toFile().length() + if (currentSize > (assetLimitInMB * SIZE_OF_1MB)) { + isRecording = false + scope.launch { + _maxFileSizeReached.emit( + RecordAudioDialogState.MaxFileSizeReached( + maxSize = assetLimitInMB / SIZE_OF_1MB + ) ) - ) + } + break } } + + // Close buffer to ensure all data is written + bufferedSink.close() + + // Update WAV header with final file size + updateWavHeader(originalOutputPath!!) + } catch (e: IOException) { + e.printStackTrace() + appLogger.e("[RecordAudio] writeAudioDataToFile: IOException - ${e.message}") + } finally { + sink?.close() } } - private companion object { - fun getRecordingAudioFileName(): String = - "wire-audio-${DateTimeUtil.currentInstant().fileDateTime()}.m4a" - fun getRecordingAudioEffectsFileName(): String = - "wire-audio-${DateTimeUtil.currentInstant().fileDateTime()}-filter.m4a" + companion object { + fun getRecordingAudioFileName(): String = "wire-audio-${DateTimeUtil.currentInstant().fileDateTime()}.wav" + fun getRecordingAudioEffectsFileName(): String = "wire-audio-${DateTimeUtil.currentInstant().fileDateTime()}-filter.wav" + const val SIZE_OF_1MB = 1024 * 1024 - const val AUDIO_CHANNELS = 1 + const val AUDIO_CHANNELS = 1 // Mono const val SAMPLING_RATE = 44100 - const val AUDIO_ENCONDING_BIT_RATE = 96000 + const val BUFFER_SIZE = 1024 + const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT + const val BITS_PER_SAMPLE = 16 + const val BITS_PER_BYTE = 8 + const val HEADER_SIZE = 44 + const val CHUNK_ID_SIZE = 8 + const val INT_SIZE = 4 + const val PLACEHOLDER_SIZE = 0 + const val CHUNK_SIZE_OFFSET = 4 + const val SUBCHUNK2_SIZE_OFFSET = 40 + const val AUDIO_FORMAT_PCM = 1 + const val SUBCHUNK1_SIZE_PCM = 16 + + const val CHUNK_ID_RIFF = "RIFF" + const val FORMAT_WAVE = "WAVE" + const val SUBCHUNK1_ID_FMT = "fmt " + const val SUBCHUNK2_ID_DATA = "data" } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/GenerateAudioFileWithEffectsUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/GenerateAudioFileWithEffectsUseCase.kt index 59562af397d..432b65ff2ba 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/GenerateAudioFileWithEffectsUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/GenerateAudioFileWithEffectsUseCase.kt @@ -20,29 +20,38 @@ package com.wire.android.ui.home.messagecomposer.recordaudio import android.content.Context import com.waz.audioeffect.AudioEffect import com.wire.android.appLogger -import javax.inject.Singleton +import com.wire.android.util.dispatchers.DispatcherProvider +import kotlinx.coroutines.withContext import javax.inject.Inject +import javax.inject.Singleton @Singleton -class GenerateAudioFileWithEffectsUseCase @Inject constructor() { +class GenerateAudioFileWithEffectsUseCase @Inject constructor( + private val dispatchers: DispatcherProvider, +) { /** * Note: This UseCase can't be tested as we cannot mock `AudioEffect` from AVS. * Generates audio file with effects on received path from the original file path. * * @return Unit, as the content of audio with effects will be saved directly to received file path. */ - operator fun invoke( + suspend operator fun invoke( context: Context, originalFilePath: String, - effectsFilePath: String - ) { - val audioEffectsResult = AudioEffect(context) - .applyEffectM4A( - originalFilePath, - effectsFilePath, - AudioEffect.AVS_AUDIO_EFFECT_VOCODER_MED, - true - ) + effectsFilePath: String, + ) = withContext(dispatchers.io()) { + appLogger.i("[$TAG] -> Start generating audio file with effects") + + val audioEffect = AudioEffect(context) + val effectType = AudioEffect.AVS_AUDIO_EFFECT_VOCODER_MED + val reduceNoise = true + + val audioEffectsResult = audioEffect.applyEffectWav( + originalFilePath, + effectsFilePath, + effectType, + reduceNoise + ) if (audioEffectsResult > -1) { appLogger.i("[$TAG] -> Audio file with effects generated successfully.") diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioButtons.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioButtons.kt index f67487e3128..33e010dc9b1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioButtons.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioButtons.kt @@ -51,6 +51,7 @@ import androidx.compose.ui.unit.sp import com.wire.android.R import com.wire.android.media.audiomessage.AudioMediaPlayingState import com.wire.android.media.audiomessage.AudioState +import com.wire.android.ui.common.button.IconAlignment import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.button.WireTertiaryIconButton @@ -68,7 +69,7 @@ import java.io.File @Composable fun RecordAudioButtonClose( onClick: () -> Unit, - modifier: Modifier + modifier: Modifier = Modifier ) { WireTertiaryIconButton( onButtonClicked = onClick, @@ -86,7 +87,7 @@ fun RecordAudioButtonEnabled( applyAudioFilterState: Boolean, applyAudioFilterClick: (Boolean) -> Unit, onClick: () -> Unit, - modifier: Modifier + modifier: Modifier = Modifier ) { RecordAudioButton( onClick = onClick, @@ -105,7 +106,7 @@ fun RecordAudioButtonEnabled( fun RecordAudioButtonRecording( applyAudioFilterState: Boolean, onClick: () -> Unit, - modifier: Modifier + modifier: Modifier = Modifier ) { var seconds by remember { mutableStateOf(0) @@ -149,15 +150,48 @@ fun RecordAudioButtonRecording( ) } +@Composable +fun RecordAudioButtonEncoding( + applyAudioFilterState: Boolean, + modifier: Modifier = Modifier +) { + if (!LocalInspectionMode.current) { + val activity = LocalContext.current as Activity + + DisposableEffect(Unit) { + activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + onDispose { + activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + } + + RecordAudioButton( + onClick = {}, + modifier = modifier, + topContent = {}, + iconResId = null, + trailingIconAlignment = IconAlignment.Center, + contentDescription = -1, + buttonColor = colorsScheme().recordAudioStopColor, + bottomText = R.string.record_audio_encoding_label, + buttonState = WireButtonState.Disabled, + isAudioFilterEnabled = false, + loading = true, + applyAudioFilterState = applyAudioFilterState, + applyAudioFilterClick = { } + ) +} + @Composable fun RecordAudioButtonSend( applyAudioFilterState: Boolean, audioState: AudioState, onClick: () -> Unit, - modifier: Modifier, outputFile: File?, onPlayAudio: () -> Unit, onSliderPositionChange: (Int) -> Unit, + modifier: Modifier = Modifier, applyAudioFilterClick: (Boolean) -> Unit ) { RecordAudioButton( @@ -188,17 +222,19 @@ fun RecordAudioButtonSend( @Composable private fun RecordAudioButton( onClick: () -> Unit, - modifier: Modifier, topContent: @Composable () -> Unit, - @DrawableRes iconResId: Int, + @DrawableRes iconResId: Int?, @StringRes contentDescription: Int, buttonColor: Color, @StringRes bottomText: Int, - buttonState: WireButtonState = WireButtonState.Default, applyAudioFilterState: Boolean, applyAudioFilterClick: (Boolean) -> Unit, - isAudioFilterEnabled: Boolean = true -) { + modifier: Modifier = Modifier, + buttonState: WireButtonState = WireButtonState.Default, + isAudioFilterEnabled: Boolean = true, + loading: Boolean = false, + trailingIconAlignment: IconAlignment = IconAlignment.Border, + ) { Column( modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally @@ -210,20 +246,24 @@ private fun RecordAudioButton( modifier = Modifier .width(dimensions().spacing80x) .height(dimensions().spacing80x), + trailingIconAlignment = trailingIconAlignment, onClick = onClick, - leadingIcon = { - Icon( - painter = painterResource(id = iconResId), - contentDescription = stringResource(id = contentDescription), - modifier = Modifier.size(dimensions().spacing20x), - tint = colorsScheme().onPrimary - ) + leadingIcon = iconResId?.let { + { + Icon( + painter = painterResource(id = it), + contentDescription = stringResource(id = contentDescription), + modifier = Modifier.size(dimensions().spacing20x), + tint = colorsScheme().onPrimary + ) + } }, shape = CircleShape, colors = wireSecondaryButtonColors().copy( enabled = buttonColor ), - state = buttonState + state = buttonState, + loading = loading ) Spacer(modifier = Modifier.height(dimensions().spacing16x)) Text( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioComponent.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioComponent.kt index 99c5ca31c7f..ca2ff4e6a5c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioComponent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioComponent.kt @@ -132,6 +132,11 @@ fun RecordAudioComponent( onPlayAudio = viewModel::onPlayAudio, onSliderPositionChange = viewModel::onSliderPositionChange ) + + RecordAudioButtonState.ENCODING -> RecordAudioButtonEncoding( + applyAudioFilterState = viewModel.state.shouldApplyEffects, + modifier = buttonModifier + ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioState.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioState.kt index 7d60ee2c4e5..7e6d654053c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioState.kt @@ -46,7 +46,12 @@ enum class RecordAudioButtonState { /** * READY_TO_SEND: When User finished recording its audio message. */ - READY_TO_SEND + READY_TO_SEND, + + /** + * ENCODING: When recorded audio is encoding + */ + ENCODING } sealed class RecordAudioDialogState { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModel.kt index 244490f61bc..024c6e9a0eb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModel.kt @@ -30,9 +30,9 @@ import com.wire.android.media.audiomessage.AudioMediaPlayingState import com.wire.android.media.audiomessage.AudioState import com.wire.android.media.audiomessage.RecordAudioMessagePlayer import com.wire.android.ui.home.conversations.model.UriAsset -import com.wire.android.util.AUDIO_MIME_TYPE import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager +import com.wire.android.util.SUPPORTED_AUDIO_MIME_TYPE import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.getAudioLengthInMs import com.wire.android.util.ui.UIText @@ -159,8 +159,8 @@ class RecordAudioViewModel @Inject constructor( audioMediaRecorder.setUp(assetSizeLimit) if (audioMediaRecorder.startRecording()) { state = state.copy( - originalOutputFile = audioMediaRecorder.originalOutputFile, - effectsOutputFile = audioMediaRecorder.effectsOutputFile, + originalOutputFile = audioMediaRecorder.originalOutputPath!!.toFile(), + effectsOutputFile = audioMediaRecorder.effectsOutputPath!!.toFile(), buttonState = RecordAudioButtonState.RECORDING ) } else { @@ -173,11 +173,17 @@ class RecordAudioViewModel @Inject constructor( fun stopRecording() { viewModelScope.launch(dispatchers.default()) { if (state.buttonState == RecordAudioButtonState.RECORDING) { + appLogger.i("[$tag] -> Stopping audioMediaRecorder") audioMediaRecorder.stop() } + appLogger.i("[$tag] -> Releasing audioMediaRecorder") audioMediaRecorder.release() if (state.originalOutputFile != null && state.effectsOutputFile != null) { + state = state.copy( + buttonState = RecordAudioButtonState.ENCODING, + audioState = state.audioState.copy(audioMediaPlayingState = AudioMediaPlayingState.Fetching) + ) generateAudioFileWithEffects( context = context, originalFilePath = state.originalOutputFile!!.path, @@ -191,7 +197,7 @@ class RecordAudioViewModel @Inject constructor( getPlayableAudioFile()?.let { getAudioLengthInMs( dataPath = it.path.toPath(), - mimeType = AUDIO_MIME_TYPE + mimeType = SUPPORTED_AUDIO_MIME_TYPE ).toInt() } ?: 0 ) @@ -205,7 +211,8 @@ class RecordAudioViewModel @Inject constructor( when (state.buttonState) { RecordAudioButtonState.ENABLED -> onCloseRecordAudio() RecordAudioButtonState.RECORDING, - RecordAudioButtonState.READY_TO_SEND -> { + RecordAudioButtonState.READY_TO_SEND, + RecordAudioButtonState.ENCODING -> { state = state.copy( discardDialogState = RecordAudioDialogState.Shown ) diff --git a/app/src/main/kotlin/com/wire/android/util/FileUtil.kt b/app/src/main/kotlin/com/wire/android/util/FileUtil.kt index 667b5765e5e..9ca2ede4651 100644 --- a/app/src/main/kotlin/com/wire/android/util/FileUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/FileUtil.kt @@ -437,4 +437,4 @@ fun getAudioLengthInMs(dataPath: Path, mimeType: String): Long = private const val ATTACHMENT_FILENAME = "attachment" private const val DATA_COPY_BUFFER_SIZE = 2048 const val SDK_VERSION = 33 -const val AUDIO_MIME_TYPE = "audio/mp4" +const val SUPPORTED_AUDIO_MIME_TYPE = "audio/wav" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fdfb715a6bb..25431f98d77 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1327,6 +1327,7 @@ In group conversations, the group admin can overwrite this setting. Start Recording Recording Audio… + Encoding Audio… Send Audio Message Apply audio filter Discard Audio Message? diff --git a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelTest.kt index ba1bf91ab05..59d7c44460a 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelTest.kt @@ -106,17 +106,17 @@ class RecordAudioViewModelTest { viewModel.stopRecording() // then - assertEquals( - RecordAudioButtonState.READY_TO_SEND, - viewModel.state.buttonState - ) - verify(exactly = 1) { + coVerify(exactly = 1) { arrangement.generateAudioFileWithEffects( context = any(), originalFilePath = viewModel.state.originalOutputFile!!.path, effectsFilePath = viewModel.state.effectsOutputFile!!.path ) } + assertEquals( + RecordAudioButtonState.READY_TO_SEND, + viewModel.state.buttonState + ) } @Test @@ -305,18 +305,16 @@ class RecordAudioViewModelTest { every { audioMediaRecorder.stop() } returns Unit every { audioMediaRecorder.release() } returns Unit every { globalDataStore.isRecordAudioEffectsCheckboxEnabled() } returns flowOf(false) - every { audioMediaRecorder.originalOutputFile } returns fakeKaliumFileSystem - .tempFilePath("temp_recording.mp3") - .toFile() - every { audioMediaRecorder.effectsOutputFile } returns fakeKaliumFileSystem - .tempFilePath("temp_recording_effects.mp3") - .toFile() + every { audioMediaRecorder.originalOutputPath } returns fakeKaliumFileSystem + .tempFilePath("temp_recording.wav") + every { audioMediaRecorder.effectsOutputPath } returns fakeKaliumFileSystem + .tempFilePath("temp_recording_effects.wav") coEvery { audioMediaRecorder.getMaxFileSizeReached() } returns flowOf( RecordAudioDialogState.MaxFileSizeReached( maxSize = GetAssetSizeLimitUseCaseImpl.ASSET_SIZE_DEFAULT_LIMIT_BYTES ) ) - every { generateAudioFileWithEffects(any(), any(), any()) } returns Unit + coEvery { generateAudioFileWithEffects(any(), any(), any()) } returns Unit coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow( CurrentScreen.Conversation(