diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml
index 75a63bff19b..54b3bf8bc75 100644
--- a/app/lint-baseline.xml
+++ b/app/lint-baseline.xml
@@ -267,7 +267,7 @@
+
+
+
+
-
-
-
-
@@ -4078,18 +4078,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
-
-
-
-
@@ -5090,7 +5079,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -5277,7 +5266,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -6007,61 +5996,6 @@
column="5"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -6788,39 +6711,6 @@
column="5"/>
-
-
-
-
-
-
-
-
-
-
-
-
@@ -7888,17 +7778,6 @@
column="27"/>
-
-
-
-
@@ -8049,7 +7928,7 @@
errorLine2=" ^">
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 f0c3f6d6c59..30ea557ccbc 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
@@ -36,7 +36,7 @@ import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase
import com.wire.android.ui.home.messagecomposer.model.ComposableMessageBundle
import com.wire.android.ui.home.messagecomposer.model.MessageBundle
import com.wire.android.ui.home.messagecomposer.model.Ping
-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
@@ -165,7 +165,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 4b66ac39640..df38ab86700 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1315,6 +1315,7 @@
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(