Model the fake voice recorder on a cancellable recording job (#7466)

* Model the fake voice recorder on a cancellable recording job

FakeVoiceRecorder pushed a whole recording synchronously from a single
suspending call, which made it both weaker and stricter than
DefaultVoiceRecorder.

Every level emission carried the same elapsed time and the emissions were
coalesced by Molecule, so no intermediate recording state was observable and
the level-limit test had to skip half the emissions to compensate. Each level
now advances the test time source and suspends for its share of the recording
duration, so the states arrive one at a time and that magic number is gone.

Call verification moved to the constructor lambdas the rest of the repo uses,
in place of hand-rolled counters and assertCalls. The counters discarded the
argument, so nothing pinned that cancelling a recording stops it with
cancelled = true; that is now asserted. Truth is no longer needed by the
fixture module.

A second startRecord() no longer raises an error where production only warns
and returns, and stopping or deleting terminates an in-flight emission loop
the way cancelling the real recording job does, so a stale recording state can
no longer arrive after the recording has finished.

* Keep each recording's level loop to its own recording

The level loop checked a single isRecording flag, which cannot tell "this
recording was cancelled" from "a later recording has started". After a cancel
followed by a new start, the first loop resumed, saw the flag set again, and
advanced the shared test time source, so the new recording reported the elapsed
time of both. Each recording now carries an id and its loop stops as soon as
that id is no longer the active one.
This commit is contained in:
hayyaksi
2026-08-19 14:44:11 +03:00
committed by GitHub
parent cf2ce2bd65
commit 7341feeed8
3 changed files with 103 additions and 76 deletions
@@ -47,6 +47,7 @@ import io.element.android.libraries.voicerecorder.api.VoiceRecorder
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilTimeout
import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
@@ -60,6 +61,7 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@Suppress("LargeClass") @Suppress("LargeClass")
@@ -67,8 +69,14 @@ class DefaultVoiceMessageComposerPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
private val startRecordResult = lambdaRecorder<Unit> { }
private val stopRecordResult = lambdaRecorder<Boolean, Unit> { }
private val deleteRecordingResult = lambdaRecorder<Unit> { }
private val voiceRecorder = FakeVoiceRecorder( private val voiceRecorder = FakeVoiceRecorder(
recordingDuration = RECORDING_DURATION recordingDuration = RECORDING_DURATION,
startRecordResult = startRecordResult,
stopRecordResult = stopRecordResult,
deleteRecordingResult = deleteRecordingResult,
) )
private val analyticsService = FakeAnalyticsService() private val analyticsService = FakeAnalyticsService()
private val sendVoiceMessageResult = private val sendVoiceMessageResult =
@@ -97,7 +105,9 @@ class DefaultVoiceMessageComposerPresenterTest {
companion object { companion object {
private val RECORDING_DURATION = 1.seconds private val RECORDING_DURATION = 1.seconds
private val FIRST_LEVEL_DURATION = 500.milliseconds
private val RECORDING_STATE = VoiceMessageState.Recording(RECORDING_DURATION, listOf(0.1f, 0.2f).toImmutableList()) private val RECORDING_STATE = VoiceMessageState.Recording(RECORDING_DURATION, listOf(0.1f, 0.2f).toImmutableList())
private val FIRST_RECORDING_STATE = VoiceMessageState.Recording(FIRST_LEVEL_DURATION, listOf(0.1f).toImmutableList())
} }
@Test @Test
@@ -106,7 +116,7 @@ class DefaultVoiceMessageComposerPresenterTest {
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
voiceRecorder.assertCalls(started = 0) startRecordResult.assertions().isNeverCalled()
testPauseAndDestroy(initialState) testPauseAndDestroy(initialState)
} }
@@ -118,9 +128,10 @@ class DefaultVoiceMessageComposerPresenterTest {
presenter.test { presenter.test {
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
assertThat(awaitItem().voiceMessageState).isEqualTo(FIRST_RECORDING_STATE)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
voiceRecorder.assertCalls(started = 1) startRecordResult.assertions().isCalledOnce()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
@@ -133,14 +144,16 @@ class DefaultVoiceMessageComposerPresenterTest {
val voiceRecorder = FakeVoiceRecorder( val voiceRecorder = FakeVoiceRecorder(
levels = levels, levels = levels,
recordingDuration = RECORDING_DURATION, recordingDuration = RECORDING_DURATION,
startRecordResult = { },
stopRecordResult = { },
deleteRecordingResult = { },
) )
val presenter = createDefaultVoiceMessageComposerPresenter( val presenter = createDefaultVoiceMessageComposerPresenter(
voiceRecorder = voiceRecorder, voiceRecorder = voiceRecorder,
) )
presenter.test { presenter.test {
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
skipItems(numberOfLevels / 2 - 1) val finalState = consumeItemsUntilTimeout().last()
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isInstanceOf(VoiceMessageState.Recording::class.java) assertThat(finalState.voiceMessageState).isInstanceOf(VoiceMessageState.Recording::class.java)
val recordingState = finalState.voiceMessageState as VoiceMessageState.Recording val recordingState = finalState.voiceMessageState as VoiceMessageState.Recording
// The number of levels should be limited to 128 items // The number of levels should be limited to 128 items
@@ -221,7 +234,8 @@ class DefaultVoiceMessageComposerPresenterTest {
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState()) assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState())
voiceRecorder.assertCalls(started = 1, stopped = 1) startRecordResult.assertions().isCalledOnce()
stopRecordResult.assertions().isCalledOnce().with(value(false))
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
@@ -235,7 +249,9 @@ class DefaultVoiceMessageComposerPresenterTest {
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Cancel)) awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Cancel))
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) startRecordResult.assertions().isCalledOnce()
stopRecordResult.assertions().isCalledOnce().with(value(true))
deleteRecordingResult.assertions().isCalledOnce()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
} }
@@ -249,7 +265,9 @@ class DefaultVoiceMessageComposerPresenterTest {
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState()) assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState())
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) startRecordResult.assertions().isCalledOnce()
stopRecordResult.assertions().isCalledOnce().with(value(false))
deleteRecordingResult.assertions().isNeverCalled()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
@@ -265,8 +283,10 @@ class DefaultVoiceMessageComposerPresenterTest {
} }
// Nothing should happen // Nothing should happen
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) assertThat(finalState.voiceMessageState).isEqualTo(FIRST_RECORDING_STATE)
voiceRecorder.assertCalls(started = 1, stopped = 0, deleted = 0) startRecordResult.assertions().isCalledOnce()
stopRecordResult.assertions().isNeverCalled()
deleteRecordingResult.assertions().isNeverCalled()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
@@ -282,7 +302,9 @@ class DefaultVoiceMessageComposerPresenterTest {
val finalState = awaitItem().also { val finalState = awaitItem().also {
assertThat(it.voiceMessageState).isEqualTo(aPlayingState()) assertThat(it.voiceMessageState).isEqualTo(aPlayingState())
} }
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) startRecordResult.assertions().isCalledOnce()
stopRecordResult.assertions().isCalledOnce().with(value(false))
deleteRecordingResult.assertions().isNeverCalled()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
@@ -299,7 +321,9 @@ class DefaultVoiceMessageComposerPresenterTest {
val finalState = awaitItem().also { val finalState = awaitItem().also {
assertThat(it.voiceMessageState).isEqualTo(aPausedState()) assertThat(it.voiceMessageState).isEqualTo(aPausedState())
} }
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) startRecordResult.assertions().isCalledOnce()
stopRecordResult.assertions().isCalledOnce().with(value(false))
deleteRecordingResult.assertions().isNeverCalled()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
@@ -340,7 +364,9 @@ class DefaultVoiceMessageComposerPresenterTest {
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) startRecordResult.assertions().isCalledOnce()
stopRecordResult.assertions().isCalledOnce().with(value(false))
deleteRecordingResult.assertions().isCalledOnce()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
@@ -360,7 +386,9 @@ class DefaultVoiceMessageComposerPresenterTest {
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) startRecordResult.assertions().isCalledOnce()
stopRecordResult.assertions().isCalledOnce().with(value(false))
deleteRecordingResult.assertions().isCalledOnce()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
@@ -377,7 +405,9 @@ class DefaultVoiceMessageComposerPresenterTest {
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
sendVoiceMessageResult.assertions().isCalledOnce() sendVoiceMessageResult.assertions().isCalledOnce()
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) startRecordResult.assertions().isCalledOnce()
stopRecordResult.assertions().isCalledOnce().with(value(false))
deleteRecordingResult.assertions().isCalledOnce()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
@@ -455,7 +485,9 @@ class DefaultVoiceMessageComposerPresenterTest {
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
sendVoiceMessageResult.assertions().isCalledOnce() sendVoiceMessageResult.assertions().isCalledOnce()
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) startRecordResult.assertions().isCalledOnce()
stopRecordResult.assertions().isCalledOnce().with(value(false))
deleteRecordingResult.assertions().isCalledOnce()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
@@ -476,7 +508,9 @@ class DefaultVoiceMessageComposerPresenterTest {
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
sendVoiceMessageResult.assertions().isCalledOnce() sendVoiceMessageResult.assertions().isCalledOnce()
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) startRecordResult.assertions().isCalledOnce()
stopRecordResult.assertions().isCalledOnce().with(value(false))
deleteRecordingResult.assertions().isCalledOnce()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
@@ -499,7 +533,9 @@ class DefaultVoiceMessageComposerPresenterTest {
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState(isSending = true)) assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState(isSending = true))
sendVoiceMessageResult.assertions().isNeverCalled() sendVoiceMessageResult.assertions().isNeverCalled()
assertThat(analyticsService.trackedErrors).isEmpty() assertThat(analyticsService.trackedErrors).isEmpty()
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) startRecordResult.assertions().isCalledOnce()
stopRecordResult.assertions().isCalledOnce().with(value(false))
deleteRecordingResult.assertions().isNeverCalled()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
@@ -527,7 +563,9 @@ class DefaultVoiceMessageComposerPresenterTest {
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
sendVoiceMessageResult.assertions().isCalledOnce() sendVoiceMessageResult.assertions().isCalledOnce()
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) startRecordResult.assertions().isCalledOnce()
stopRecordResult.assertions().isCalledOnce().with(value(false))
deleteRecordingResult.assertions().isCalledOnce()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
@@ -577,7 +615,7 @@ class DefaultVoiceMessageComposerPresenterTest {
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
sendVoiceMessageResult.assertions().isNeverCalled() sendVoiceMessageResult.assertions().isNeverCalled()
assertThat(analyticsService.trackedErrors).hasSize(1) assertThat(analyticsService.trackedErrors).hasSize(1)
voiceRecorder.assertCalls(started = 0) startRecordResult.assertions().isNeverCalled()
testPauseAndDestroy(initialState) testPauseAndDestroy(initialState)
} }
@@ -586,8 +624,14 @@ class DefaultVoiceMessageComposerPresenterTest {
@Test @Test
fun `present - record error - security exceptions are tracked`() = runTest { fun `present - record error - security exceptions are tracked`() = runTest {
val exception = SecurityException("") val exception = SecurityException("")
voiceRecorder.givenThrowsSecurityException(exception) val startRecordResult = lambdaRecorder<Unit> { throw exception }
val presenter = createDefaultVoiceMessageComposerPresenter() val voiceRecorder = FakeVoiceRecorder(
recordingDuration = RECORDING_DURATION,
startRecordResult = startRecordResult,
)
val presenter = createDefaultVoiceMessageComposerPresenter(
voiceRecorder = voiceRecorder,
)
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
@@ -596,7 +640,7 @@ class DefaultVoiceMessageComposerPresenterTest {
assertThat(analyticsService.trackedErrors).containsExactly( assertThat(analyticsService.trackedErrors).containsExactly(
VoiceMessageException.PermissionMissing(message = "Expected permission to record but none", cause = exception) VoiceMessageException.PermissionMissing(message = "Expected permission to record but none", cause = exception)
) )
voiceRecorder.assertCalls(started = 1) startRecordResult.assertions().isCalledOnce()
testPauseAndDestroy(initialState) testPauseAndDestroy(initialState)
} }
@@ -616,7 +660,8 @@ class DefaultVoiceMessageComposerPresenterTest {
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle)
initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
voiceRecorder.assertCalls(stopped = 1) startRecordResult.assertions().isNeverCalled()
stopRecordResult.assertions().isCalledOnce().with(value(false))
permissionsPresenter.setPermissionGranted() permissionsPresenter.setPermissionGranted()
@@ -625,7 +670,8 @@ class DefaultVoiceMessageComposerPresenterTest {
val finalState = expectMostRecentItem() val finalState = expectMostRecentItem()
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
voiceRecorder.assertCalls(stopped = 1, started = 1) startRecordResult.assertions().isCalledOnce()
stopRecordResult.assertions().isCalledOnce().with(value(false))
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
@@ -659,7 +705,7 @@ class DefaultVoiceMessageComposerPresenterTest {
val finalState = expectMostRecentItem() val finalState = expectMostRecentItem()
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
voiceRecorder.assertCalls(started = 1) startRecordResult.assertions().isCalledOnce()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
@@ -696,7 +742,7 @@ class DefaultVoiceMessageComposerPresenterTest {
assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle) assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
assertThat(it.showPermissionRationaleDialog).isTrue() assertThat(it.showPermissionRationaleDialog).isTrue()
} }
voiceRecorder.assertCalls(started = 0) startRecordResult.assertions().isNeverCalled()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
@@ -19,6 +19,5 @@ dependencies {
implementation(projects.tests.testutils) implementation(projects.tests.testutils)
implementation(libs.coroutines.test) implementation(libs.coroutines.test)
implementation(libs.test.truth)
implementation(projects.libraries.core) implementation(projects.libraries.core)
} }
@@ -8,13 +8,13 @@
package io.element.android.libraries.voicerecorder.test package io.element.android.libraries.voicerecorder.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.voicerecorder.api.VoiceRecorder import io.element.android.libraries.voicerecorder.api.VoiceRecorder
import io.element.android.libraries.voicerecorder.api.VoiceRecorderState import io.element.android.libraries.voicerecorder.api.VoiceRecorderState
import io.element.android.tests.testutils.lambda.lambdaError
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.yield
import java.io.File import java.io.File
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -23,51 +23,47 @@ import kotlin.time.TestTimeSource
class FakeVoiceRecorder( class FakeVoiceRecorder(
private val timeSource: TestTimeSource = TestTimeSource(), private val timeSource: TestTimeSource = TestTimeSource(),
private val recordingDuration: Duration = 0.seconds, private val recordingDuration: Duration = 0.seconds,
private val levels: List<Float> = listOf(0.1f, 0.2f) private val levels: List<Float> = listOf(0.1f, 0.2f),
val waveform: List<Float> = A_WAVEFORM,
private val startRecordResult: () -> Unit = { lambdaError() },
private val stopRecordResult: (Boolean) -> Unit = { lambdaError() },
private val deleteRecordingResult: () -> Unit = { lambdaError() },
) : VoiceRecorder { ) : VoiceRecorder {
private val _state = MutableStateFlow<VoiceRecorderState>(VoiceRecorderState.Idle) private val _state = MutableStateFlow<VoiceRecorderState>(VoiceRecorderState.Idle)
override val state: StateFlow<VoiceRecorderState> = _state override val state: StateFlow<VoiceRecorderState> = _state
private var curRecording: File? = null private val levelInterval = if (levels.isEmpty()) Duration.ZERO else recordingDuration / levels.size
private var securityException: SecurityException? = null private var currentRecording: File? = null
private var lastRecordingId = 0
private var activeRecordingId = 0
private var startedCount = 0
private var stoppedCount = 0
private var deletedCount = 0
var waveform: List<Float> = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f)
override suspend fun startRecord() { override suspend fun startRecord() {
startedCount += 1 startRecordResult()
if (activeRecordingId != 0) return
val recordingId = ++lastRecordingId
activeRecordingId = recordingId
currentRecording = File("file.ogg")
val startedAt = timeSource.markNow() val startedAt = timeSource.markNow()
securityException?.let { throw it }
if (curRecording != null) {
error("Previous recording was not cleared")
}
curRecording = File("file.ogg")
timeSource += recordingDuration
for (i in 1..levels.size) { for (i in 1..levels.size) {
delay(levelInterval)
if (activeRecordingId != recordingId) return
timeSource += levelInterval
_state.emit(VoiceRecorderState.Recording(startedAt.elapsedNow(), levels.take(i))) _state.emit(VoiceRecorderState.Recording(startedAt.elapsedNow(), levels.take(i)))
yield()
} }
} }
override suspend fun stopRecord( override suspend fun stopRecord(cancelled: Boolean) {
cancelled: Boolean stopRecordResult(cancelled)
) { activeRecordingId = 0
stoppedCount++
if (cancelled) { if (cancelled) {
deleteRecording() deleteRecording()
} }
_state.emit( _state.emit(
when (curRecording) { when (val file = currentRecording) {
null -> VoiceRecorderState.Idle null -> VoiceRecorderState.Idle
else -> VoiceRecorderState.Finished( else -> VoiceRecorderState.Finished(
file = curRecording!!, file = file,
mimeType = MimeTypes.Ogg, mimeType = MimeTypes.Ogg,
duration = recordingDuration, duration = recordingDuration,
waveform = waveform, waveform = waveform,
@@ -77,25 +73,11 @@ class FakeVoiceRecorder(
} }
override suspend fun deleteRecording() { override suspend fun deleteRecording() {
deletedCount++ deleteRecordingResult()
curRecording = null activeRecordingId = 0
currentRecording = null
_state.emit( _state.emit(VoiceRecorderState.Idle)
VoiceRecorderState.Idle
)
}
fun assertCalls(
started: Int = 0,
stopped: Int = 0,
deleted: Int = 0,
) {
assertThat(startedCount).isEqualTo(started)
assertThat(stoppedCount).isEqualTo(stopped)
assertThat(deletedCount).isEqualTo(deleted)
}
fun givenThrowsSecurityException(exception: SecurityException) {
this.securityException = exception
} }
} }
private val A_WAVEFORM = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f)