Store voice player playback index in a datastore.

This commit is contained in:
Benoit Marty
2025-12-31 11:28:26 +01:00
parent 325d7d5fde
commit 152b351bf3
6 changed files with 88 additions and 6 deletions

View File

@@ -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)

View File

@@ -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,

View File

@@ -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<VoiceMessageState> {
private val play = mutableStateOf<AsyncData<Unit>>(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,
)
}

View File

@@ -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<Int>
}
@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<Int> {
return store.data.map { prefs ->
prefs[playbackSpeedIndex] ?: 0
}
}
override suspend fun setPlayBackSpeedIndex(index: Int) {
store.edit { prefs ->
prefs[playbackSpeedIndex] = index
}
}
}

View File

@@ -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<Int> {
return playBackSpeedIndex.asStateFlow()
}
override suspend fun setPlayBackSpeedIndex(index: Int) {
playBackSpeedIndex.emit(index)
}
}

View File

@@ -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,
)