diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt index 0a85df5ed17..b78a9e14ea7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt @@ -141,7 +141,10 @@ class VoiceMessageComposerPresenter @Inject constructor( return VoiceMessageComposerState( voiceMessageState = when (val state = recorderState) { - is VoiceRecorderState.Recording -> VoiceMessageState.Recording(level = state.level) + is VoiceRecorderState.Recording -> VoiceMessageState.Recording( + duration = state.elapsedTime, + level = state.level + ) is VoiceRecorderState.Finished -> if (isSending) { VoiceMessageState.Sending } else { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt index 1a904beee30..502485526e9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt @@ -18,11 +18,12 @@ package io.element.android.features.messages.impl.voicemessages import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.textcomposer.model.VoiceMessageState +import kotlin.time.Duration.Companion.seconds internal open class VoiceMessageComposerStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(level = 0.5)), + aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(duration = 61.seconds, level = 0.5)), ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt index 8fbcd5e76f2..95a1cad3cae 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt @@ -46,18 +46,26 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import kotlin.time.Duration.Companion.seconds class VoiceMessageComposerPresenterTest { @get:Rule val warmUpRule = WarmUpRule() - private val voiceRecorder = FakeVoiceRecorder() + private val voiceRecorder = FakeVoiceRecorder( + recordingDuration = RECORDING_DURATION + ) private val analyticsService = FakeAnalyticsService() private val matrixRoom = FakeMatrixRoom() private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() } private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom) + companion object { + private val RECORDING_DURATION = 1.seconds + private val RECORDING_STATE = VoiceMessageState.Recording(RECORDING_DURATION, 0.2) + } + @Test fun `present - initial state`() = runTest { val presenter = createVoiceMessageComposerPresenter() @@ -80,7 +88,7 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) val finalState = awaitItem() - assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Recording(0.2)) + assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) testPauseAndDestroy(finalState) } @@ -270,7 +278,7 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) val finalState = awaitItem() - assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Recording(0.2)) + assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) testPauseAndDestroy(finalState) } @@ -303,7 +311,7 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) val finalState = awaitItem() - assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Recording(0.2)) + assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) testPauseAndDestroy(finalState) } diff --git a/libraries/textcomposer/impl/build.gradle.kts b/libraries/textcomposer/impl/build.gradle.kts index bdc5e2b3c52..db97a967870 100644 --- a/libraries/textcomposer/impl/build.gradle.kts +++ b/libraries/textcomposer/impl/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.libraries.testtags) + implementation(projects.libraries.uiUtils) implementation(libs.matrix.richtexteditor) api(libs.matrix.richtexteditor.compose) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index b67d4f458e5..691d3b9c9f4 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -80,6 +80,7 @@ import io.element.android.wysiwyg.compose.RichTextEditor import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlin.time.Duration.Companion.seconds @Composable fun TextComposer( @@ -181,7 +182,7 @@ fun TextComposer( VoiceMessageState.Sending -> VoiceMessagePreview(isInteractive = false) is VoiceMessageState.Recording -> - VoiceMessageRecording(voiceMessageState.level) + VoiceMessageRecording(voiceMessageState.level, voiceMessageState.duration) VoiceMessageState.Idle -> {} } } @@ -751,7 +752,7 @@ internal fun TextComposerVoicePreview() = ElementPreview { enableVoiceMessages = true, ) PreviewColumn(items = persistentListOf({ - VoicePreview(voiceMessageState = VoiceMessageState.Recording(0.5)) + VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, 0.5)) }, { VoicePreview(voiceMessageState = VoiceMessageState.Preview) }, { diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt index 24703a579ce..99a0e82b7ce 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt @@ -36,10 +36,14 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.utils.time.formatShort +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds @Composable internal fun VoiceMessageRecording( level: Double, + duration: Duration, modifier: Modifier = Modifier, ) { Row( @@ -53,16 +57,13 @@ internal fun VoiceMessageRecording( .heightIn(26.dp), verticalAlignment = Alignment.CenterVertically, ) { - Box( - modifier = Modifier - .size(8.dp) - .background(color = ElementTheme.colors.textCriticalPrimary, shape = CircleShape) - ) + RedRecordingDot() + Spacer(Modifier.size(8.dp)) - // TODO Replace with timer UI + // Timer Text( - text = "Recording...", // Not localized because it is a placeholder + text = duration.formatShort(), color = ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodySmMedium ) @@ -95,8 +96,17 @@ private fun DebugAudioLevel( } } +@Composable +private fun RedRecordingDot( + modifier: Modifier = Modifier, +) = Box( + modifier = modifier + .size(8.dp) + .background(color = ElementTheme.colors.textCriticalPrimary, shape = CircleShape) +) + @PreviewsDayNight @Composable internal fun VoiceMessageRecordingPreview() = ElementPreview { - VoiceMessageRecording(0.5) + VoiceMessageRecording(0.5, 0.seconds) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt index 9a15791cb5d..590d5f2e509 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt @@ -16,12 +16,15 @@ package io.element.android.libraries.textcomposer.model +import kotlin.time.Duration + sealed class VoiceMessageState { data object Idle: VoiceMessageState() data object Preview: VoiceMessageState() data object Sending: VoiceMessageState() data class Recording( + val duration: Duration, val level: Double, ): VoiceMessageState() } diff --git a/libraries/ui-utils/build.gradle.kts b/libraries/ui-utils/build.gradle.kts new file mode 100644 index 00000000000..26759fa9c8d --- /dev/null +++ b/libraries/ui-utils/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.ui.utils" + + dependencies { + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + } +} diff --git a/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/time/DurationExt.kt b/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/time/DurationExt.kt new file mode 100644 index 00000000000..57f7a22af3f --- /dev/null +++ b/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/time/DurationExt.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.ui.utils.time + +import kotlin.time.Duration + +/** + * Format a duration as minutes:seconds. + * + * For example, + * - 0 seconds will be formatted as "0:00". + * - 65 seconds will be formatted as "1:05". + * - 2 hours will be formatted as "120:00". + * - negative 10 seconds will be formatted as "-0:10". + * + * @return the formatted duration. + */ +fun Duration.formatShort(): String { + // Format as minutes:seconds + val seconds = (absoluteValue.inWholeSeconds % 60) + .toString() + .padStart(2, '0') + + val sign = isNegative().let { if (it) "-" else "" } + + return "$sign${absoluteValue.inWholeMinutes}:$seconds" +} diff --git a/libraries/ui-utils/src/test/kotlin/io/element/android/libraries/ui/utils/time/DurationFormatTest.kt b/libraries/ui-utils/src/test/kotlin/io/element/android/libraries/ui/utils/time/DurationFormatTest.kt new file mode 100644 index 00000000000..dce9239b597 --- /dev/null +++ b/libraries/ui-utils/src/test/kotlin/io/element/android/libraries/ui/utils/time/DurationFormatTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.ui.utils.time + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import kotlin.time.Duration.Companion.seconds + +@RunWith(value = Parameterized::class) +class DurationFormatTest( + private val seconds: Double, + private val output: String, +) { + companion object { + @Parameterized.Parameters(name = "{index}: format({0})={1}") + @JvmStatic + fun data(): Iterable> { + return arrayListOf( + arrayOf(0, "0:00"), + arrayOf(1, "0:01"), + arrayOf(10, "0:10"), + arrayOf(59.9, "0:59"), + arrayOf(60, "1:00"), + arrayOf(61, "1:01"), + arrayOf(60 * 60, "60:00"), + arrayOf(-60, "-1:00"), + arrayOf(-1, "-0:01"), + ).toList() + } + } + + @Test + fun formatShort() { + assertEquals(output, seconds.seconds.formatShort()) + } +} diff --git a/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt index 8d531c3565a..6ba1476ac70 100644 --- a/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt +++ b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.voicerecorder.api import java.io.File +import kotlin.time.Duration sealed class VoiceRecorderState { /** @@ -27,9 +28,10 @@ sealed class VoiceRecorderState { /** * The recorder is currently recording. * + * @property elapsedTime The elapsed time since the recording started. * @property level The current audio level of the recording as a fraction of 1. */ - data class Recording(val level: Double) : VoiceRecorderState() + data class Recording(val elapsedTime: Duration, val level: Double) : VoiceRecorderState() /** * The recorder has finished recording. diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt index ef911183716..b150cd1059a 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt @@ -37,15 +37,19 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.yield import timber.log.Timber import java.io.File import java.util.UUID import javax.inject.Inject +import kotlin.time.Duration.Companion.minutes +import kotlin.time.TimeSource @SingleIn(RoomScope::class) @ContributesBinding(RoomScope::class) class VoiceRecorderImpl @Inject constructor( private val dispatchers: CoroutineDispatchers, + private val timeSource: TimeSource, private val audioReaderFactory: AudioReader.Factory, private val encoder: Encoder, private val fileManager: VoiceFileManager, @@ -74,16 +78,27 @@ class VoiceRecorderImpl @Inject constructor( val audioRecorder = audioReaderFactory.create(config, dispatchers).also { audioReader = it } recordingJob = voiceCoroutineScope.launch { + val startedAt = timeSource.markNow() audioRecorder.record { audio -> + yield() + + val elapsedTime = startedAt.elapsedNow() + + if (elapsedTime >= 30.minutes) { + Timber.w("Voice message time limit reached") + stopRecord(false) + return@record + } + when (audio) { is Audio.Data -> { val audioLevel = audioLevelCalculator.calculateAudioLevel(audio.buffer) - _state.emit(VoiceRecorderState.Recording(audioLevel)) + _state.emit(VoiceRecorderState.Recording(elapsedTime, audioLevel)) encoder.encode(audio.buffer, audio.readSize) } is Audio.Error -> { Timber.e("Voice message error: code=${audio.audioRecordErrorCode}") - _state.emit(VoiceRecorderState.Recording(0.0)) + _state.emit(VoiceRecorderState.Recording(elapsedTime, 0.0)) } } } diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt index 847e1c514f4..5c9e0506ab5 100644 --- a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt @@ -37,9 +37,13 @@ import kotlinx.coroutines.test.runTest import org.junit.BeforeClass import org.junit.Test import java.io.File +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TestTimeSource class VoiceRecorderImplTest { private val fakeFileSystem = FakeFileSystem() + private val timeSource = TestTimeSource() @Test fun `it emits the initial state`() = runTest { @@ -56,9 +60,27 @@ class VoiceRecorderImplTest { assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) voiceRecorder.startRecord() - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.0)) - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.0)) - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.0)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.seconds, 1.0)) + timeSource += 1.seconds + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.seconds,0.0)) + timeSource += 1.seconds + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(2.seconds, 1.0)) + } + } + + @Test + fun `when elapsed time reaches 30 minutes, it stops recording`() = runTest { + val voiceRecorder = createVoiceRecorder() + voiceRecorder.state.test { + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) + + voiceRecorder.startRecord() + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.minutes, 1.0)) + timeSource += 29.minutes + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(29.minutes, 0.0)) + timeSource += 1.minutes + + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Finished(File(FILE_PATH), "audio/ogg")) } } @@ -94,6 +116,7 @@ class VoiceRecorderImplTest { val fileConfig = VoiceRecorderModule.provideVoiceFileConfig() return VoiceRecorderImpl( dispatchers = testCoroutineDispatchers(), + timeSource = timeSource, audioReaderFactory = FakeAudioRecorderFactory( audio = AUDIO, ), diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioReader.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioReader.kt index 71fd2df0413..fecae4dbd59 100644 --- a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioReader.kt +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioReader.kt @@ -35,6 +35,7 @@ class FakeAudioReader( while (audios.hasNext()) { if (!isRecording) break onAudio(audios.next()) + yield() } while (isActive) { // do not return from the coroutine until it is cancelled diff --git a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt index f7659a119b5..6df2c6c0fe5 100644 --- a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt +++ b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt @@ -21,8 +21,13 @@ import io.element.android.libraries.voicerecorder.api.VoiceRecorderState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import java.io.File +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TestTimeSource class FakeVoiceRecorder( + private val timeSource: TestTimeSource = TestTimeSource(), + private val recordingDuration: Duration = 0.seconds, private val levels: List = listOf(0.1, 0.2) ) : VoiceRecorder { private val _state = MutableStateFlow(VoiceRecorderState.Idle) @@ -33,6 +38,7 @@ class FakeVoiceRecorder( private var securityException: SecurityException? = null override suspend fun startRecord() { + val startedAt = timeSource.markNow() securityException?.let { throw it } if (curRecording != null) { @@ -40,8 +46,9 @@ class FakeVoiceRecorder( } curRecording = File("file.ogg") + timeSource += recordingDuration levels.forEach { - _state.emit(VoiceRecorderState.Recording(it)) + _state.emit(VoiceRecorderState.Recording(startedAt.elapsedNow(), it)) } } diff --git a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/TimeModule.kt b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/TimeModule.kt new file mode 100644 index 00000000000..afaed29a290 --- /dev/null +++ b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/TimeModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.services.toolbox.impl.systemclock + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import io.element.android.libraries.di.AppScope +import kotlin.time.TimeSource + +@Module +@ContributesTo(AppScope::class) +object TimeModule { + @Provides + fun timeSource(): TimeSource { + return TimeSource.Monotonic + } +} diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png index d8b8529f7c4..9b798ded601 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34c05059f7c4f997f3c4af339f548773aafbbcb688d563d559c14c75fba1c70d -size 9039 +oid sha256:3c25252b8d43f4ffb58673f375709b4811901a78676580a7dd0288b8624615d7 +size 7787 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png index de2aafdcaba..26978d7efea 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd5d861bd3630a0341e6f8ef9bf16eb4135ed770bf549d4713cedb476c974cb2 -size 8634 +oid sha256:55fa9c5633d3776a3401db72e2e53d9eacedd745e0db7f259f6ada5d7a8d584a +size 7473 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-15_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-15_15_null,NEXUS_5,1.0,en].png index b710fa6f09d..1436eea3f51 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-15_15_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-15_15_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a06a32b3b576c01074eb14f236f8043d209600fb098fdd3933b01f99055c24d -size 8069 +oid sha256:208ad62e23efd6f07e2cce08c1d1511af4c4fc2ae6bc3299134fed9efb1c55d3 +size 7238 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-15_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-15_16_null,NEXUS_5,1.0,en].png index b3cf4e0ccad..4b2b7194981 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-15_16_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-15_16_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd347e9e28f7518bd4e4169426d9172ef7a107895d9fb93744daf47bf1ae0e50 -size 7681 +oid sha256:8cf7662c33de6ad1b58785674cbfce1d6323fe84e35c8398b563ea65e5ff9fa7 +size 6919 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png index d6ee084bb04..ac0cc6d3abd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1bba507fdd9fc8526408d176f0519f42179a3618ff8e8a41f25b36a13a1a00f -size 18323 +oid sha256:2c2eadc9585070e9a07dda78145c337a003166d4dc990e794d71ece0f8cab4f3 +size 17134 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png index 87256c52f49..d1650ebc7fd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c268e0a5bbdabb2355cb44820f666d2c4f298e5eeda7720328ad08ac9ac7cdc5 -size 17357 +oid sha256:8443556fa3ad8ca7c928689b3fdd2db43cec62ceb8db340353c90cbd7ea76355 +size 16228