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

View file

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

View file

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

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