From 835293314d35e171172e9028f13a4538ace4c69b Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 6 Nov 2024 03:22:33 +0900 Subject: [PATCH 1/5] Fix state flow of mute UI updates on Android --- .../ferrostar/core/NavigationViewModel.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt index 0c2a3ed8..76f118fb 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt @@ -8,10 +8,13 @@ import com.stadiamaps.ferrostar.core.extensions.deviation import com.stadiamaps.ferrostar.core.extensions.progress import com.stadiamaps.ferrostar.core.extensions.remainingSteps import com.stadiamaps.ferrostar.core.extensions.visualInstruction +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import uniffi.ferrostar.GeographicCoordinate import uniffi.ferrostar.RouteDeviation import uniffi.ferrostar.RouteStep @@ -99,10 +102,12 @@ class DefaultNavigationViewModel( ) : ViewModel(), NavigationViewModel { private var userLocation: UserLocation? = locationProvider.lastLocation + private var muteState: MutableStateFlow = + MutableStateFlow(spokenInstructionObserver?.isMuted) override val uiState = - ferrostarCore.state - .map { coreState -> + combine(ferrostarCore.state, muteState) { a, b -> a to b } + .map { (coreState, muteState) -> val location = locationProvider.lastLocation userLocation = when (coreState.tripState) { @@ -110,7 +115,7 @@ class DefaultNavigationViewModel( is TripState.Complete, TripState.Idle -> locationProvider.lastLocation } - uiState(coreState, spokenInstructionObserver?.isMuted, location, userLocation) + uiState(coreState, muteState, location, userLocation) // This awkward dance is required because Kotlin doesn't have a way to map over // StateFlows // without converting to a generic Flow in the process. @@ -134,7 +139,10 @@ class DefaultNavigationViewModel( Log.d("NavigationViewModel", "Spoken instruction observer is null, mute operation ignored.") return } - spokenInstructionObserver.isMuted = !spokenInstructionObserver.isMuted + muteState.update { oldValue -> + spokenInstructionObserver.isMuted = !spokenInstructionObserver.isMuted + spokenInstructionObserver.isMuted + } } // TODO: We can add a hook here to override the current road name. From b0ee153cae35f1c1c928068930b9918cb2c7dcca Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 6 Nov 2024 09:52:21 +0900 Subject: [PATCH 2/5] Update android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt Co-authored-by: Jacob Fielding --- .../java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt index 76f118fb..b9c41908 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt @@ -103,7 +103,7 @@ class DefaultNavigationViewModel( private var userLocation: UserLocation? = locationProvider.lastLocation private var muteState: MutableStateFlow = - MutableStateFlow(spokenInstructionObserver?.isMuted) + remember { mutableStateOf(spokenInstructionObserver?.isMuted) } override val uiState = combine(ferrostarCore.state, muteState) { a, b -> a to b } From 2295e28543391423b77c401e775762b9a5ace98f Mon Sep 17 00:00:00 2001 From: ianthetechie Date: Wed, 6 Nov 2024 00:53:39 +0000 Subject: [PATCH 3/5] Apply automatic changes --- .../com/stadiamaps/ferrostar/core/NavigationViewModel.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt index b9c41908..510abd5d 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt @@ -102,8 +102,9 @@ class DefaultNavigationViewModel( ) : ViewModel(), NavigationViewModel { private var userLocation: UserLocation? = locationProvider.lastLocation - private var muteState: MutableStateFlow = - remember { mutableStateOf(spokenInstructionObserver?.isMuted) } + private var muteState: MutableStateFlow = remember { + mutableStateOf(spokenInstructionObserver?.isMuted) + } override val uiState = combine(ferrostarCore.state, muteState) { a, b -> a to b } From 879a2be8ee95b55321fde45f7022bcb36dcd589c Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 6 Nov 2024 10:10:36 +0900 Subject: [PATCH 4/5] Revert change; can't work outside a Composable context --- .../com/stadiamaps/ferrostar/core/NavigationViewModel.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt index 510abd5d..76f118fb 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt @@ -102,9 +102,8 @@ class DefaultNavigationViewModel( ) : ViewModel(), NavigationViewModel { private var userLocation: UserLocation? = locationProvider.lastLocation - private var muteState: MutableStateFlow = remember { - mutableStateOf(spokenInstructionObserver?.isMuted) - } + private var muteState: MutableStateFlow = + MutableStateFlow(spokenInstructionObserver?.isMuted) override val uiState = combine(ferrostarCore.state, muteState) { a, b -> a to b } From fee48cea5ba350e2c474ccbc079ccb9dde929144 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 6 Nov 2024 10:59:51 +0900 Subject: [PATCH 5/5] Refactor speech observer data flow --- .../ferrostar/core/NavigationViewModel.kt | 10 +++----- .../com/stadiamaps/ferrostar/core/Speech.kt | 23 +++++++++++++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt index 76f118fb..f381482a 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt @@ -14,7 +14,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update import uniffi.ferrostar.GeographicCoordinate import uniffi.ferrostar.RouteDeviation import uniffi.ferrostar.RouteStep @@ -102,8 +101,8 @@ class DefaultNavigationViewModel( ) : ViewModel(), NavigationViewModel { private var userLocation: UserLocation? = locationProvider.lastLocation - private var muteState: MutableStateFlow = - MutableStateFlow(spokenInstructionObserver?.isMuted) + private val muteState: StateFlow = + spokenInstructionObserver?.muteState ?: MutableStateFlow(null) override val uiState = combine(ferrostarCore.state, muteState) { a, b -> a to b } @@ -139,10 +138,7 @@ class DefaultNavigationViewModel( Log.d("NavigationViewModel", "Spoken instruction observer is null, mute operation ignored.") return } - muteState.update { oldValue -> - spokenInstructionObserver.isMuted = !spokenInstructionObserver.isMuted - spokenInstructionObserver.isMuted - } + spokenInstructionObserver.setMuted(!spokenInstructionObserver.isMuted) } // TODO: We can add a hook here to override the current road name. diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/Speech.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/Speech.kt index 2a101247..d1d6713b 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/Speech.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/Speech.kt @@ -3,6 +3,10 @@ package com.stadiamaps.ferrostar.core import android.content.Context import android.speech.tts.TextToSpeech import android.speech.tts.TextToSpeech.OnInitListener +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import uniffi.ferrostar.SpokenInstruction interface SpokenInstructionObserver { @@ -17,7 +21,12 @@ interface SpokenInstructionObserver { /** Stops speech and clears the queue of spoken utterances. */ fun stopAndClearQueue() - var isMuted: Boolean + fun setMuted(isMuted: Boolean) + + val muteState: StateFlow + + val isMuted: Boolean + get() = muteState.value } /** Observes the status of an [AndroidTtsObserver]. */ @@ -62,14 +71,18 @@ class AndroidTtsObserver( private const val TAG = "AndroidTtsObserver" } - override var isMuted: Boolean = false - set(value) { - field = value + private var _muteState: MutableStateFlow = MutableStateFlow(false) - if (value && tts?.isSpeaking == true) { + override fun setMuted(isMuted: Boolean) { + _muteState.update { _ -> + if (isMuted && tts?.isSpeaking == true) { tts?.stop() } + isMuted } + } + + override val muteState: StateFlow = _muteState.asStateFlow() var tts: TextToSpeech? private set