Store voice player playback index in a datastore.

This commit is contained in:
Benoit Marty
2025-12-31 11:28:26 +01:00
parent e30efc1e2c
commit 987779af66
6 changed files with 88 additions and 6 deletions
@@ -26,10 +26,12 @@ dependencies {
implementation(projects.libraries.di) implementation(projects.libraries.di)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.mediaplayer.api) implementation(projects.libraries.mediaplayer.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.uiUtils) implementation(projects.libraries.uiUtils)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
implementation(libs.androidx.annotationjvm) implementation(libs.androidx.annotationjvm)
implementation(libs.androidx.datastore.preferences)
implementation(libs.coroutines.core) implementation(libs.coroutines.core)
testCommonDependencies(libs) testCommonDependencies(libs)
@@ -26,6 +26,7 @@ class DefaultVoiceMessagePresenterFactory(
@SessionCoroutineScope @SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
private val voiceMessagePlayerFactory: VoiceMessagePlayer.Factory, private val voiceMessagePlayerFactory: VoiceMessagePlayer.Factory,
private val voicePlayerStore: VoicePlayerStore,
) : VoiceMessagePresenterFactory { ) : VoiceMessagePresenterFactory {
override fun createVoiceMessagePresenter( override fun createVoiceMessagePresenter(
eventId: EventId?, eventId: EventId?,
@@ -44,6 +45,7 @@ class DefaultVoiceMessagePresenterFactory(
return VoiceMessagePresenter( return VoiceMessagePresenter(
analyticsService = analyticsService, analyticsService = analyticsService,
sessionCoroutineScope = sessionCoroutineScope, sessionCoroutineScope = sessionCoroutineScope,
voicePlayerStore = voicePlayerStore,
player = player, player = player,
eventId = eventId, eventId = eventId,
duration = duration, duration = duration,
@@ -9,12 +9,13 @@
package io.element.android.libraries.voiceplayer.impl package io.element.android.libraries.voiceplayer.impl
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.architecture.runUpdatingState
@@ -34,15 +35,16 @@ import kotlin.time.Duration.Companion.milliseconds
class VoiceMessagePresenter( class VoiceMessagePresenter(
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
private val voicePlayerStore: VoicePlayerStore,
private val player: VoiceMessagePlayer, private val player: VoiceMessagePlayer,
private val eventId: EventId?, private val eventId: EventId?,
private val duration: Duration, private val duration: Duration,
) : Presenter<VoiceMessageState> { ) : Presenter<VoiceMessageState> {
private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
private val playbackSpeedIndex = mutableIntStateOf(0)
@Composable @Composable
override fun present(): VoiceMessageState { override fun present(): VoiceMessageState {
val localCoroutineScope = rememberCoroutineScope()
val playerState by player.state.collectAsState( val playerState by player.state.collectAsState(
VoiceMessagePlayer.State( VoiceMessagePlayer.State(
isReady = false, 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 { val buttonType by remember {
derivedStateOf { derivedStateOf {
when { when {
@@ -114,9 +122,10 @@ class VoiceMessagePresenter(
is VoiceMessageEvent.Seek -> { is VoiceMessageEvent.Seek -> {
player.seekTo((event.percentage * duration).toLong()) player.seekTo((event.percentage * duration).toLong())
} }
is VoiceMessageEvent.ChangePlaybackSpeed -> { is VoiceMessageEvent.ChangePlaybackSpeed -> localCoroutineScope.launch {
playbackSpeedIndex.intValue = (playbackSpeedIndex.intValue + 1) % VoicePlayerConfig.availablePlaybackSpeeds.size voicePlayerStore.setPlayBackSpeedIndex(
player.setPlaybackSpeed(VoicePlayerConfig.availablePlaybackSpeeds[playbackSpeedIndex.intValue]) (playbackSpeedIndex + 1) % VoicePlayerConfig.availablePlaybackSpeeds.size
)
} }
} }
} }
@@ -126,7 +135,7 @@ class VoiceMessagePresenter(
progress = progress, progress = progress,
time = time, time = time,
showCursor = showCursor, showCursor = showCursor,
playbackSpeed = VoicePlayerConfig.availablePlaybackSpeeds[playbackSpeedIndex.intValue], playbackSpeed = VoicePlayerConfig.availablePlaybackSpeeds[playbackSpeedIndex],
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }
@@ -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
}
}
}
@@ -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)
}
}
@@ -240,6 +240,7 @@ fun TestScope.createVoiceMessagePresenter(
mediaPlayer: FakeMediaPlayer = FakeMediaPlayer(), mediaPlayer: FakeMediaPlayer = FakeMediaPlayer(),
voiceMessageMediaRepo: VoiceMessageMediaRepo = FakeVoiceMessageMediaRepo(), voiceMessageMediaRepo: VoiceMessageMediaRepo = FakeVoiceMessageMediaRepo(),
analyticsService: AnalyticsService = FakeAnalyticsService(), analyticsService: AnalyticsService = FakeAnalyticsService(),
voicePlayerStore: VoicePlayerStore = InMemoryVoicePlayerStore(),
eventId: EventId? = EventId("\$anEventId"), eventId: EventId? = EventId("\$anEventId"),
filename: String = "filename doesn't really matter for a voice message", filename: String = "filename doesn't really matter for a voice message",
duration: Duration = 61_000.milliseconds, duration: Duration = 61_000.milliseconds,
@@ -257,6 +258,7 @@ fun TestScope.createVoiceMessagePresenter(
mimeType = mimeType, mimeType = mimeType,
filename = filename filename = filename
), ),
voicePlayerStore = voicePlayerStore,
eventId = eventId, eventId = eventId,
duration = duration, duration = duration,
) )