Merge pull request #7346 from hayaksi1/fix/5780-voice-recorder-double-start

Ignore a second voice recording start while one is already running
This commit is contained in:
Benoit Marty
2026-08-11 16:20:20 +02:00
committed by GitHub
3 changed files with 39 additions and 7 deletions
@@ -71,14 +71,17 @@ class DefaultVoiceRecorder(
override val state: StateFlow<VoiceRecorderState> = _state override val state: StateFlow<VoiceRecorderState> = _state
@RequiresPermission(Manifest.permission.RECORD_AUDIO) @RequiresPermission(Manifest.permission.RECORD_AUDIO)
override suspend fun startRecord() { override suspend fun startRecord() = lock.withLock {
if (recordingJob != null) {
Timber.w("Voice recorder is already recording, ignoring this start")
return@withLock
}
Timber.i("Voice recorder started recording") Timber.i("Voice recorder started recording")
outputFile = fileManager.createFile() outputFile = fileManager.createFile()
.also(encoder::init) .also(encoder::init)
lock.withLock { levels.clear()
levels.clear()
}
val audioRecorder = audioReaderFactory.create(config, dispatchers).also { audioReader = it } val audioRecorder = audioReaderFactory.create(config, dispatchers).also { audioReader = it }
@@ -39,6 +39,7 @@ import kotlin.time.TestTimeSource
class DefaultVoiceRecorderTest { class DefaultVoiceRecorderTest {
private val fakeFileSystem = FakeFileSystem() private val fakeFileSystem = FakeFileSystem()
private val timeSource = TestTimeSource() private val timeSource = TestTimeSource()
private val audioReaderFactory = FakeAudioReaderFactory(audio = AUDIO)
@Test @Test
fun `it emits the initial state`() = runTest { fun `it emits the initial state`() = runTest {
@@ -109,6 +110,32 @@ class DefaultVoiceRecorderTest {
} }
} }
@Test
fun `when startRecord is called twice, the second call is ignored`() = runTest {
val voiceRecorder = createDefaultVoiceRecorder()
voiceRecorder.state.test {
assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle)
voiceRecorder.startRecord()
voiceRecorder.startRecord()
assertThat(audioReaderFactory.createdCount).isEqualTo(1)
skipItems(1)
timeSource += 5.seconds
skipItems(2)
voiceRecorder.stopRecord()
assertThat(awaitItem()).isEqualTo(
VoiceRecorderState.Finished(
file = File(FILE_PATH),
mimeType = MimeTypes.Ogg,
waveform = List(100) { 1f },
duration = 5.seconds,
)
)
expectNoEvents()
}
}
@Test @Test
fun `when cancelled, it deletes the file`() = runTest { fun `when cancelled, it deletes the file`() = runTest {
val voiceRecorder = createDefaultVoiceRecorder() val voiceRecorder = createDefaultVoiceRecorder()
@@ -128,9 +155,7 @@ class DefaultVoiceRecorderTest {
return DefaultVoiceRecorder( return DefaultVoiceRecorder(
dispatchers = testCoroutineDispatchers(), dispatchers = testCoroutineDispatchers(),
timeSource = timeSource, timeSource = timeSource,
audioReaderFactory = FakeAudioReaderFactory( audioReaderFactory = audioReaderFactory,
audio = AUDIO,
),
encoder = FakeEncoder(fakeFileSystem), encoder = FakeEncoder(fakeFileSystem),
config = AudioConfig( config = AudioConfig(
format = audioFormat, format = audioFormat,
@@ -16,7 +16,11 @@ import io.element.android.libraries.voicerecorder.impl.audio.AudioReader
class FakeAudioReaderFactory( class FakeAudioReaderFactory(
private val audio: List<Audio> private val audio: List<Audio>
) : AudioReader.Factory { ) : AudioReader.Factory {
var createdCount: Int = 0
private set
override fun create(config: AudioConfig, dispatchers: CoroutineDispatchers): AudioReader { override fun create(config: AudioConfig, dispatchers: CoroutineDispatchers): AudioReader {
createdCount++
return FakeAudioReader(dispatchers, audio) return FakeAudioReader(dispatchers, audio)
} }
} }