Skip to content

Commit 79a27f0

Browse files
committed
Update audio guidance for AA compatibility
1 parent af7d19b commit 79a27f0

File tree

18 files changed

+225
-223
lines changed

18 files changed

+225
-223
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Mapbox welcomes participation and contributions from everyone.
44

55
## Unreleased
66
#### Features
7+
- Moved `MapboxAudioGuidance` and `MapboxAudioGuidanceState` into public api. [#6336](https://github.com/mapbox/mapbox-navigation-android/pull/6336)
78
#### Bug fixes and improvements
89

910
## Mapbox Navigation SDK 2.8.0-rc.1 - 15 September, 2022

libnavui-androidauto/src/main/java/com/mapbox/androidauto/MapboxCarApp.kt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
package com.mapbox.androidauto
22

3-
import android.app.Application
43
import com.mapbox.androidauto.navigation.location.CarAppLocation
54
import com.mapbox.androidauto.navigation.location.impl.CarAppLocationImpl
65
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
76
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
8-
import com.mapbox.navigation.ui.app.internal.SharedApp
9-
import com.mapbox.navigation.ui.voice.internal.MapboxAudioGuidance
7+
import com.mapbox.navigation.ui.voice.api.MapboxAudioGuidance
108
import kotlinx.coroutines.flow.MutableStateFlow
119
import kotlinx.coroutines.flow.StateFlow
1210

1311
/**
1412
* The entry point for your Mapbox Android Auto app.
1513
*/
16-
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
1714
object MapboxCarApp {
1815

1916
private val carAppStateFlow = MutableStateFlow<CarAppState>(FreeDriveState)
@@ -43,12 +40,15 @@ object MapboxCarApp {
4340
}
4441

4542
/**
46-
* Setup android auto from your [Application.onCreate]
47-
*
48-
* @param application used to detect when activities are foregrounded
43+
* Setup android auto with defaults
4944
*/
50-
fun setup(application: Application) {
51-
SharedApp.setup(application)
52-
MapboxNavigationApp.registerObserver(CarAppLocationImpl())
45+
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
46+
fun setup() {
47+
if (MapboxNavigationApp.getObservers(MapboxAudioGuidance::class).isEmpty()) {
48+
MapboxNavigationApp.registerObserver(MapboxAudioGuidance())
49+
}
50+
if (MapboxNavigationApp.getObservers(CarAppLocation::class).isEmpty()) {
51+
MapboxNavigationApp.registerObserver(CarAppLocationImpl())
52+
}
5353
}
5454
}

libnavui-androidauto/src/main/java/com/mapbox/androidauto/navigation/audioguidance/AppAudioGuidanceUi.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import androidx.lifecycle.LifecycleOwner
88
import androidx.lifecycle.lifecycleScope
99
import androidx.lifecycle.repeatOnLifecycle
1010
import com.mapbox.androidauto.MapboxCarApp
11-
import com.mapbox.navigation.ui.voice.internal.MapboxAudioGuidance
11+
import com.mapbox.navigation.ui.voice.api.MapboxAudioGuidanceState
1212
import com.mapbox.navigation.ui.voice.view.MapboxSoundButton
1313
import kotlinx.coroutines.flow.collect
1414
import kotlinx.coroutines.launch
@@ -40,7 +40,7 @@ fun Fragment.attachAudioGuidance(
4040
*/
4141
fun Lifecycle.muteAudioGuidance() {
4242
addObserver(object : DefaultLifecycleObserver {
43-
lateinit var initialState: MapboxAudioGuidance.State
43+
lateinit var initialState: MapboxAudioGuidanceState
4444
override fun onResume(owner: LifecycleOwner) {
4545
with(MapboxCarApp.carAppAudioGuidanceService()) {
4646
initialState = stateFlow().value

libnavui-app/src/main/java/com/mapbox/navigation/ui/app/internal/SharedApp.kt

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ import com.mapbox.navigation.ui.app.internal.controller.TripSessionStarterStateC
1818
import com.mapbox.navigation.ui.maps.internal.ui.RouteAlternativeComponent
1919
import com.mapbox.navigation.ui.maps.internal.ui.RouteAlternativeContract
2020
import com.mapbox.navigation.ui.utils.internal.datastore.NavigationDataStoreOwner
21-
import com.mapbox.navigation.ui.voice.internal.MapboxAudioGuidance
22-
import com.mapbox.navigation.ui.voice.internal.impl.MapboxAudioGuidanceImpl
21+
import com.mapbox.navigation.ui.voice.api.MapboxAudioGuidance
2322
import java.util.concurrent.atomic.AtomicBoolean
2423

2524
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
@@ -56,7 +55,6 @@ object SharedApp {
5655
@JvmOverloads
5756
fun setup(
5857
context: Context,
59-
audioGuidance: MapboxAudioGuidance? = null,
6058
routeAlternativeContract: RouteAlternativeContract? = null
6159
) {
6260
if (isSetup) return
@@ -69,7 +67,9 @@ object SharedApp {
6967
}
7068
)
7169
MapboxNavigationApp.lifecycleOwner.attachCreated(*navigationObservers)
72-
MapboxNavigationApp.registerObserver(audioGuidance ?: defaultAudioGuidance(context))
70+
if (MapboxNavigationApp.getObservers(MapboxAudioGuidance::class).isEmpty()) {
71+
MapboxNavigationApp.registerObserver(MapboxAudioGuidance())
72+
}
7373
}
7474

7575
fun tripSessionTransaction(updateSession: () -> Unit) {
@@ -80,12 +80,4 @@ object SharedApp {
8080
updateSession()
8181
ignoreTripSessionUpdates.set(false)
8282
}
83-
84-
private fun defaultAudioGuidance(context: Context): MapboxAudioGuidance {
85-
return MapboxAudioGuidanceImpl.create(context).also {
86-
it.dataStoreOwner = NavigationDataStoreOwner(context, DEFAULT_DATA_STORE_NAME)
87-
}
88-
}
89-
90-
private const val DEFAULT_DATA_STORE_NAME = "mapbox_navigation_preferences"
9183
}

libnavui-app/src/main/java/com/mapbox/navigation/ui/app/internal/controller/AudioGuidanceStateController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import com.mapbox.navigation.ui.app.internal.State
88
import com.mapbox.navigation.ui.app.internal.Store
99
import com.mapbox.navigation.ui.app.internal.audioguidance.AudioAction
1010
import com.mapbox.navigation.ui.app.internal.audioguidance.AudioGuidanceState
11-
import com.mapbox.navigation.ui.voice.internal.MapboxAudioGuidance
11+
import com.mapbox.navigation.ui.voice.api.MapboxAudioGuidance
1212

1313
/**
1414
* This class is responsible for playing voice instructions. Use the [AudioAction] to turning the

libnavui-voice/api/current.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,27 @@ package com.mapbox.navigation.ui.voice.api {
2121
method public operator void invoke(boolean result);
2222
}
2323

24+
public final class MapboxAudioGuidance implements com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver {
25+
ctor public MapboxAudioGuidance();
26+
method public void mute();
27+
method public void onAttached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation);
28+
method public void onDetached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation);
29+
method public kotlinx.coroutines.flow.StateFlow<com.mapbox.navigation.ui.voice.api.MapboxAudioGuidanceState> stateFlow();
30+
method public void toggle();
31+
method public void unmute();
32+
}
33+
34+
public final class MapboxAudioGuidanceState {
35+
method public com.mapbox.navigation.ui.voice.model.SpeechAnnouncement? getSpeechAnnouncement();
36+
method public com.mapbox.api.directions.v5.models.VoiceInstructions? getVoiceInstructions();
37+
method public boolean isMuted();
38+
method public boolean isPlayable();
39+
property public final boolean isMuted;
40+
property public final boolean isPlayable;
41+
property public final com.mapbox.navigation.ui.voice.model.SpeechAnnouncement? speechAnnouncement;
42+
property public final com.mapbox.api.directions.v5.models.VoiceInstructions? voiceInstructions;
43+
}
44+
2445
public final class MapboxSpeechApi {
2546
ctor public MapboxSpeechApi(android.content.Context context, String accessToken, String language, com.mapbox.navigation.ui.voice.options.MapboxSpeechApiOptions options = MapboxSpeechApiOptions.<init>().build());
2647
ctor public MapboxSpeechApi(android.content.Context context, String accessToken, String language);

libnavui-voice/src/main/java/com/mapbox/navigation/ui/voice/internal/impl/MapboxAudioGuidanceImpl.kt renamed to libnavui-voice/src/main/java/com/mapbox/navigation/ui/voice/api/MapboxAudioGuidance.kt

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
package com.mapbox.navigation.ui.voice.internal.impl
1+
package com.mapbox.navigation.ui.voice.api
22

3-
import android.content.Context
4-
import com.mapbox.api.directions.v5.models.VoiceInstructions
5-
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
3+
import androidx.annotation.VisibleForTesting
64
import com.mapbox.navigation.core.MapboxNavigation
5+
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
6+
import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver
77
import com.mapbox.navigation.ui.utils.internal.configuration.NavigationConfigOwner
88
import com.mapbox.navigation.ui.utils.internal.datastore.NavigationDataStoreOwner
99
import com.mapbox.navigation.ui.utils.internal.datastore.booleanDataStoreKey
10-
import com.mapbox.navigation.ui.voice.internal.MapboxAudioGuidance
11-
import com.mapbox.navigation.ui.voice.internal.MapboxAudioGuidanceServices
1210
import com.mapbox.navigation.ui.voice.internal.MapboxAudioGuidanceVoice
13-
import com.mapbox.navigation.ui.voice.model.SpeechAnnouncement
11+
import com.mapbox.navigation.ui.voice.internal.impl.MapboxAudioGuidanceServices
1412
import kotlinx.coroutines.CoroutineDispatcher
1513
import kotlinx.coroutines.CoroutineScope
1614
import kotlinx.coroutines.Dispatchers
@@ -35,30 +33,41 @@ import kotlinx.coroutines.launch
3533
/**
3634
* Implementation of [MapboxAudioGuidance]. See interface for details.
3735
*/
38-
@ExperimentalPreviewMapboxNavigationAPI
39-
class MapboxAudioGuidanceImpl(
36+
class MapboxAudioGuidance
37+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
38+
internal constructor(
4039
private val audioGuidanceServices: MapboxAudioGuidanceServices,
41-
private val configOwner: NavigationConfigOwner,
42-
dispatcher: CoroutineDispatcher = Dispatchers.Main,
43-
) : MapboxAudioGuidance {
40+
dispatcher: CoroutineDispatcher,
41+
) : MapboxNavigationObserver {
4442

45-
var dataStoreOwner: NavigationDataStoreOwner? = null
43+
constructor() : this(MapboxAudioGuidanceServices(), Dispatchers.Main)
4644

45+
private var dataStoreOwner: NavigationDataStoreOwner? = null
46+
private var configOwner: NavigationConfigOwner? = null
4747
private var mutedStateFlow = MutableStateFlow(false)
4848
private val internalStateFlow = MutableStateFlow(MapboxAudioGuidanceState())
4949
private val scope = CoroutineScope(SupervisorJob() + dispatcher)
5050
private val mapboxVoiceInstructions = audioGuidanceServices.mapboxVoiceInstructions()
5151

5252
private var job: Job? = null
5353

54+
/**
55+
* @see [MapboxNavigationApp]
56+
*/
5457
override fun onAttached(mapboxNavigation: MapboxNavigation) {
58+
val context = mapboxNavigation.navigationOptions.applicationContext
59+
dataStoreOwner = audioGuidanceServices.dataStoreOwner(context, DEFAULT_DATA_STORE_NAME)
60+
configOwner = audioGuidanceServices.configOwner(context)
5561
mapboxVoiceInstructions.registerObservers(mapboxNavigation)
5662
job = scope.launch {
5763
restoreMutedState()
5864
audioGuidanceFlow(mapboxNavigation).collect()
5965
}
6066
}
6167

68+
/**
69+
* @see [MapboxNavigationApp]
70+
*/
6271
override fun onDetached(mapboxNavigation: MapboxNavigation) {
6372
mapboxVoiceInstructions.unregisterObservers(mapboxNavigation)
6473
job?.cancel()
@@ -73,12 +82,12 @@ class MapboxAudioGuidanceImpl(
7382
*
7483
* You can also control audio guidance by calling [mute], [unmute] or [toggle]
7584
*/
76-
override fun stateFlow(): StateFlow<MapboxAudioGuidance.State> = internalStateFlow
85+
fun stateFlow(): StateFlow<MapboxAudioGuidanceState> = internalStateFlow
7786

7887
/**
7988
* Explicit call to mute the audio guidance state.
8089
*/
81-
override fun mute() {
90+
fun mute() {
8291
scope.launch {
8392
setMutedState(true)
8493
}
@@ -87,7 +96,7 @@ class MapboxAudioGuidanceImpl(
8796
/**
8897
* Explicit call to unmute the audio guidance state.
8998
*/
90-
override fun unmute() {
99+
fun unmute() {
91100
scope.launch {
92101
setMutedState(false)
93102
}
@@ -96,7 +105,7 @@ class MapboxAudioGuidanceImpl(
96105
/**
97106
* Toggle the muted state. E.g., if audio is muted, make it unmuted.
98107
*/
99-
override fun toggle() {
108+
fun toggle() {
100109
scope.launch {
101110
if (mutedStateFlow.value) {
102111
unmute()
@@ -112,10 +121,10 @@ class MapboxAudioGuidanceImpl(
112121
@OptIn(ExperimentalCoroutinesApi::class)
113122
private fun audioGuidanceFlow(
114123
mapboxNavigation: MapboxNavigation
115-
): Flow<MapboxAudioGuidance.State> {
124+
): Flow<MapboxAudioGuidanceState> {
116125
return combine(
117126
mapboxVoiceInstructions.voiceLanguage(),
118-
configOwner.language(),
127+
configOwner!!.language(),
119128
) { voiceLanguage, deviceLanguage -> voiceLanguage ?: deviceLanguage }
120129
.distinctUntilChanged()
121130
.flatMapLatest { language ->
@@ -134,7 +143,7 @@ class MapboxAudioGuidanceImpl(
134143
/**
135144
* This flow will monitor navigation state to determine if audio is available.
136145
*/
137-
private fun silentFlow(): Flow<MapboxAudioGuidance.State> {
146+
private fun silentFlow(): Flow<MapboxAudioGuidanceState> {
138147
return mapboxVoiceInstructions.voiceInstructions()
139148
.map { state ->
140149
internalStateFlow.updateAndGet {
@@ -153,7 +162,7 @@ class MapboxAudioGuidanceImpl(
153162
@OptIn(FlowPreview::class)
154163
private fun speechFlow(
155164
audioGuidance: MapboxAudioGuidanceVoice
156-
): Flow<MapboxAudioGuidance.State> {
165+
): Flow<MapboxAudioGuidanceState> {
157166
return mapboxVoiceInstructions.voiceInstructions()
158167
.flatMapConcat { voice ->
159168
internalStateFlow.update {
@@ -166,7 +175,14 @@ class MapboxAudioGuidanceImpl(
166175
audioGuidance.speak(voice.voiceInstructions)
167176
}
168177
.map { speechAnnouncement ->
169-
internalStateFlow.updateAndGet { it.copy(speechAnnouncement = speechAnnouncement) }
178+
internalStateFlow.updateAndGet {
179+
MapboxAudioGuidanceState(
180+
isPlayable = it.isPlayable,
181+
isMuted = it.isMuted,
182+
voiceInstructions = it.voiceInstructions,
183+
speechAnnouncement = speechAnnouncement
184+
)
185+
}
170186
}
171187
}
172188

@@ -181,22 +197,9 @@ class MapboxAudioGuidanceImpl(
181197
dataStoreOwner?.write(STORE_AUDIO_GUIDANCE_MUTED, muted)
182198
}
183199

184-
companion object {
185-
val STORE_AUDIO_GUIDANCE_MUTED =
200+
private companion object {
201+
private val STORE_AUDIO_GUIDANCE_MUTED =
186202
booleanDataStoreKey("audio_guidance_muted", false)
187-
188-
fun create(context: Context): MapboxAudioGuidanceImpl {
189-
return MapboxAudioGuidanceImpl(
190-
MapboxAudioGuidanceServicesImpl(),
191-
NavigationConfigOwner(context)
192-
)
193-
}
203+
private const val DEFAULT_DATA_STORE_NAME = "mapbox_navigation_preferences"
194204
}
195205
}
196-
197-
private data class MapboxAudioGuidanceState(
198-
override val isPlayable: Boolean = false,
199-
override val isMuted: Boolean = false,
200-
override val voiceInstructions: VoiceInstructions? = null,
201-
override val speechAnnouncement: SpeechAnnouncement? = null,
202-
) : MapboxAudioGuidance.State
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.mapbox.navigation.ui.voice.api
2+
3+
import com.mapbox.api.directions.v5.models.VoiceInstructions
4+
import com.mapbox.navigation.ui.voice.model.SpeechAnnouncement
5+
6+
/**
7+
* Represents the state of [MapboxAudioGuidance].
8+
*/
9+
class MapboxAudioGuidanceState internal constructor(
10+
/**
11+
* When the trip session has started, and there is an active route.
12+
*/
13+
val isPlayable: Boolean = false,
14+
15+
/**
16+
* Controlled by the [MapboxAudioGuidance] service. When [MapboxAudioGuidance.mute] is called
17+
* this will be changed to true.
18+
*/
19+
val isMuted: Boolean = false,
20+
21+
/**
22+
* Once a voice instruction becomes available this will not be null.
23+
* When the state [isPlayable] and this is null, it means there are no voice instructions
24+
* on the route at this time.
25+
*/
26+
val voiceInstructions: VoiceInstructions? = null,
27+
28+
/**
29+
* After a [voiceInstructions] has been announced, this value will be emitted.
30+
* This will always be null when [isMuted] is true.
31+
*/
32+
val speechAnnouncement: SpeechAnnouncement? = null,
33+
) {
34+
35+
/**
36+
* Regenerate whenever a change is made
37+
*/
38+
override fun equals(other: Any?): Boolean {
39+
if (this === other) return true
40+
if (javaClass != other?.javaClass) return false
41+
42+
other as MapboxAudioGuidanceState
43+
44+
if (isPlayable != other.isPlayable) return false
45+
if (isMuted != other.isMuted) return false
46+
if (voiceInstructions != other.voiceInstructions) return false
47+
if (speechAnnouncement != other.speechAnnouncement) return false
48+
49+
return true
50+
}
51+
52+
/**
53+
* Regenerate whenever a change is made
54+
*/
55+
override fun hashCode(): Int {
56+
var result = isPlayable.hashCode()
57+
result = 31 * result + isMuted.hashCode()
58+
result = 31 * result + (voiceInstructions?.hashCode() ?: 0)
59+
result = 31 * result + (speechAnnouncement?.hashCode() ?: 0)
60+
return result
61+
}
62+
63+
/**
64+
* Regenerate whenever a change is made
65+
*/
66+
override fun toString(): String {
67+
return "MapboxAudioGuidanceState(" +
68+
"isPlayable=$isPlayable, " +
69+
"isMuted=$isMuted, " +
70+
"voiceInstructions=$voiceInstructions, " +
71+
"speechAnnouncement=$speechAnnouncement" +
72+
")"
73+
}
74+
}

0 commit comments

Comments
 (0)