diff --git a/libraries/voiceplayer/impl/build.gradle.kts b/libraries/voiceplayer/impl/build.gradle.kts index 679e8206d9..4aa00e188b 100644 --- a/libraries/voiceplayer/impl/build.gradle.kts +++ b/libraries/voiceplayer/impl/build.gradle.kts @@ -26,10 +26,12 @@ dependencies { implementation(projects.libraries.di) implementation(projects.libraries.matrix.api) implementation(projects.libraries.mediaplayer.api) + implementation(projects.libraries.preferences.api) implementation(projects.libraries.uiUtils) implementation(projects.services.analytics.api) implementation(libs.androidx.annotationjvm) + implementation(libs.androidx.datastore.preferences) implementation(libs.coroutines.core) testCommonDependencies(libs) diff --git a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePresenterFactory.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePresenterFactory.kt index b2518eaca0..0a262b8145 100644 --- a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePresenterFactory.kt +++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePresenterFactory.kt @@ -26,6 +26,7 @@ class DefaultVoiceMessagePresenterFactory( @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val voiceMessagePlayerFactory: VoiceMessagePlayer.Factory, + private val voicePlayerStore: VoicePlayerStore, ) : VoiceMessagePresenterFactory { override fun createVoiceMessagePresenter( eventId: EventId?, @@ -44,6 +45,7 @@ class DefaultVoiceMessagePresenterFactory( return VoiceMessagePresenter( analyticsService = analyticsService, sessionCoroutineScope = sessionCoroutineScope, + voicePlayerStore = voicePlayerStore, player = player, eventId = eventId, duration = duration, diff --git a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt index 37952d34b5..be93ab6526 100644 --- a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt +++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt @@ -9,12 +9,13 @@ package io.element.android.libraries.voiceplayer.impl import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState @@ -34,15 +35,16 @@ import kotlin.time.Duration.Companion.milliseconds class VoiceMessagePresenter( private val analyticsService: AnalyticsService, private val sessionCoroutineScope: CoroutineScope, + private val voicePlayerStore: VoicePlayerStore, private val player: VoiceMessagePlayer, private val eventId: EventId?, private val duration: Duration, ) : Presenter { private val play = mutableStateOf>(AsyncData.Uninitialized) - private val playbackSpeedIndex = mutableIntStateOf(0) @Composable override fun present(): VoiceMessageState { + val localCoroutineScope = rememberCoroutineScope() val playerState by player.state.collectAsState( VoiceMessagePlayer.State( isReady = false, @@ -53,6 +55,12 @@ class VoiceMessagePresenter( ) ) + val playbackSpeedIndex by voicePlayerStore.playBackSpeedIndex().collectAsState(0) + + LaunchedEffect(playbackSpeedIndex) { + player.setPlaybackSpeed(VoicePlayerConfig.availablePlaybackSpeeds[playbackSpeedIndex]) + } + val buttonType by remember { derivedStateOf { when { @@ -114,9 +122,10 @@ class VoiceMessagePresenter( is VoiceMessageEvent.Seek -> { player.seekTo((event.percentage * duration).toLong()) } - is VoiceMessageEvent.ChangePlaybackSpeed -> { - playbackSpeedIndex.intValue = (playbackSpeedIndex.intValue + 1) % VoicePlayerConfig.availablePlaybackSpeeds.size - player.setPlaybackSpeed(VoicePlayerConfig.availablePlaybackSpeeds[playbackSpeedIndex.intValue]) + is VoiceMessageEvent.ChangePlaybackSpeed -> localCoroutineScope.launch { + voicePlayerStore.setPlayBackSpeedIndex( + (playbackSpeedIndex + 1) % VoicePlayerConfig.availablePlaybackSpeeds.size + ) } } } @@ -126,7 +135,7 @@ class VoiceMessagePresenter( progress = progress, time = time, showCursor = showCursor, - playbackSpeed = VoicePlayerConfig.availablePlaybackSpeeds[playbackSpeedIndex.intValue], + playbackSpeed = VoicePlayerConfig.availablePlaybackSpeeds[playbackSpeedIndex], eventSink = ::handleEvent, ) } diff --git a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoicePlayerStore.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoicePlayerStore.kt new file mode 100644 index 0000000000..3c400b3e92 --- /dev/null +++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoicePlayerStore.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.voiceplayer.impl + +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +interface VoicePlayerStore { + suspend fun setPlayBackSpeedIndex(index: Int) + fun playBackSpeedIndex(): Flow +} + +@ContributesBinding(AppScope::class) +class PreferencesVoicePlayerStore( + preferenceDataStoreFactory: PreferenceDataStoreFactory, +) : VoicePlayerStore { + private val store = preferenceDataStoreFactory.create("elementx_voice_player") + private val playbackSpeedIndex = intPreferencesKey("playback_speed_index") + + override fun playBackSpeedIndex(): Flow { + return store.data.map { prefs -> + prefs[playbackSpeedIndex] ?: 0 + } + } + + override suspend fun setPlayBackSpeedIndex(index: Int) { + store.edit { prefs -> + prefs[playbackSpeedIndex] = index + } + } +} diff --git a/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/InMemoryVoicePlayerStore.kt b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/InMemoryVoicePlayerStore.kt new file mode 100644 index 0000000000..e746b5acf5 --- /dev/null +++ b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/InMemoryVoicePlayerStore.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.voiceplayer.impl + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +internal class InMemoryVoicePlayerStore( + defaultPlaybackSpeedIndex: Int = 0, +) : VoicePlayerStore { + private val playBackSpeedIndex = MutableStateFlow(defaultPlaybackSpeedIndex) + + override fun playBackSpeedIndex(): Flow { + return playBackSpeedIndex.asStateFlow() + } + + override suspend fun setPlayBackSpeedIndex(index: Int) { + playBackSpeedIndex.emit(index) + } +} diff --git a/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenterTest.kt b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenterTest.kt index e6ae760b53..8054677937 100644 --- a/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenterTest.kt +++ b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenterTest.kt @@ -240,6 +240,7 @@ fun TestScope.createVoiceMessagePresenter( mediaPlayer: FakeMediaPlayer = FakeMediaPlayer(), voiceMessageMediaRepo: VoiceMessageMediaRepo = FakeVoiceMessageMediaRepo(), analyticsService: AnalyticsService = FakeAnalyticsService(), + voicePlayerStore: VoicePlayerStore = InMemoryVoicePlayerStore(), eventId: EventId? = EventId("\$anEventId"), filename: String = "filename doesn't really matter for a voice message", duration: Duration = 61_000.milliseconds, @@ -257,6 +258,7 @@ fun TestScope.createVoiceMessagePresenter( mimeType = mimeType, filename = filename ), + voicePlayerStore = voicePlayerStore, eventId = eventId, duration = duration, )