Skip to content

Commit 2254578

Browse files
committed
NAVAND-552: predownload voice instructions
1 parent 8248c1f commit 2254578

File tree

26 files changed

+1418
-300
lines changed

26 files changed

+1418
-300
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ Mapbox welcomes participation and contributions from everyone.
2525
```
2626
- Added guarantees that route progress with `RouteProgress#currentState == OFF_ROUTE` arrives earlier than `NavigationRerouteController#reroute` is called. [#6764](https://github.com/mapbox/mapbox-navigation-android/pull/6764)
2727
- Fixed a rare `java.lang.NullPointerException: Attempt to read from field 'SpeechAnnouncement PlayCallback.announcement' on a null object reference` crash in `PlayCallback.getAnnouncement`. [#6760](https://github.com/mapbox/mapbox-navigation-android/pull/6760)
28+
- Introduced `VoiceInstructionsDownloadTrigger` `MapboxSpeechAPI#generatePredownloaded` to use predownloaded voice instructions instead of downloading them on demand. Example usage can be found in the examples directory ( see `MapboxVoiceActivity`). [#6771](https://github.com/mapbox/mapbox-navigation-android/pull/6771)
29+
- Enabled voice instructions predownloading for those who use `MapboxAudioGuidance`. [#6771](https://github.com/mapbox/mapbox-navigation-android/pull/6771)
30+
- Fixed an issue where with low connectivity voice instruction might have been played too late for those who use `MapboxAudioGuidance`. If you use `MapboxSpeechAPI` directly, switch to voice instructions predownloading as described above if you encounter said issue. [#6771](https://github.com/mapbox/mapbox-navigation-android/pull/6771)
2831

2932
## Mapbox Navigation SDK 2.10.0-rc.1 - 16 December, 2022
3033
### Changelog

examples/src/main/java/com/mapbox/navigation/examples/core/MapboxVoiceActivity.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoadRequest
6060
import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoaderFactory
6161
import com.mapbox.navigation.ui.voice.api.MapboxSpeechApi
6262
import com.mapbox.navigation.ui.voice.api.MapboxVoiceInstructionsPlayer
63+
import com.mapbox.navigation.ui.voice.api.VoiceInstructionsDownloadTrigger
6364
import com.mapbox.navigation.ui.voice.model.SpeechAnnouncement
6465
import com.mapbox.navigation.ui.voice.model.SpeechError
6566
import com.mapbox.navigation.ui.voice.model.SpeechValue
@@ -201,9 +202,13 @@ class MapboxVoiceActivity : AppCompatActivity(), OnMapLongClickListener {
201202
}
202203
}
203204

205+
private val voiceInstructionsDownloadTrigger by lazy {
206+
VoiceInstructionsDownloadTrigger(speechApi)
207+
}
208+
204209
private val voiceInstructionsObserver =
205210
VoiceInstructionsObserver { voiceInstructions -> // The data obtained must be used to generate the synthesized speech mp3 file.
206-
speechApi.generate(
211+
speechApi.generatePredownloaded(
207212
voiceInstructions,
208213
speechCallback
209214
)
@@ -389,6 +394,7 @@ class MapboxVoiceActivity : AppCompatActivity(), OnMapLongClickListener {
389394
if (::mapboxNavigation.isInitialized) {
390395
mapboxNavigation.registerRoutesObserver(routesObserver)
391396
mapboxNavigation.registerLocationObserver(locationObserver)
397+
mapboxNavigation.registerRouteProgressObserver(voiceInstructionsDownloadTrigger)
392398
mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
393399
mapboxNavigation.registerRouteProgressObserver(replayProgressObserver)
394400
mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver)
@@ -400,6 +406,7 @@ class MapboxVoiceActivity : AppCompatActivity(), OnMapLongClickListener {
400406
super.onStop()
401407
ResourceLoaderFactory.getInstance().unregisterObserver(resourceLoadObserver)
402408
mapboxNavigation.unregisterRoutesObserver(routesObserver)
409+
mapboxNavigation.registerVoiceInstructionsTriggerObserver(voiceInstructionsDownloadTrigger)
403410
mapboxNavigation.unregisterLocationObserver(locationObserver)
404411
mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
405412
mapboxNavigation.unregisterRouteProgressObserver(replayProgressObserver)
@@ -413,6 +420,7 @@ class MapboxVoiceActivity : AppCompatActivity(), OnMapLongClickListener {
413420
mapboxReplayer.finish()
414421
mapboxNavigation.onDestroy()
415422
speechApi.cancel()
423+
voiceInstructionsDownloadTrigger.destroy()
416424
voiceInstructionsPlayer.shutdown()
417425
}
418426

libnavigation-core/api/current.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ package com.mapbox.navigation.core {
5959
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void registerRoutesPreviewObserver(com.mapbox.navigation.core.preview.RoutesPreviewObserver observer);
6060
method public void registerTripSessionStateObserver(com.mapbox.navigation.core.trip.session.TripSessionStateObserver tripSessionStateObserver);
6161
method public void registerVoiceInstructionsObserver(com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver voiceInstructionsObserver);
62+
method public <T extends com.mapbox.navigation.core.directions.session.RoutesObserver & com.mapbox.navigation.core.trip.session.RouteProgressObserver> void registerVoiceInstructionsTriggerObserver(T observer);
6263
method public void requestAlternativeRoutes();
6364
method public void requestAlternativeRoutes(com.mapbox.navigation.core.routealternatives.NavigationRouteAlternativesRequestCallback? callback = null);
6465
method public void requestAlternativeRoutes(com.mapbox.navigation.core.routealternatives.RouteAlternativesRequestCallback callback);
@@ -102,6 +103,7 @@ package com.mapbox.navigation.core {
102103
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void unregisterRoutesPreviewObserver(com.mapbox.navigation.core.preview.RoutesPreviewObserver observer);
103104
method public void unregisterTripSessionStateObserver(com.mapbox.navigation.core.trip.session.TripSessionStateObserver tripSessionStateObserver);
104105
method public void unregisterVoiceInstructionsObserver(com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver voiceInstructionsObserver);
106+
method public <T extends com.mapbox.navigation.core.directions.session.RoutesObserver & com.mapbox.navigation.core.trip.session.RouteProgressObserver> void unregisterVoiceInstructionsTriggerObserver(T observer);
105107
property public final com.mapbox.navigator.Experimental experimental;
106108
property public final com.mapbox.navigation.core.trip.session.eh.GraphAccessor graphAccessor;
107109
property public final com.mapbox.navigation.core.history.MapboxHistoryRecorder historyRecorder;

libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,26 @@ class MapboxNavigation @VisibleForTesting internal constructor(
12831283
}
12841284
}
12851285

1286+
/**
1287+
* Subscribes voice instructions trigger observer for all the necessary updates.
1288+
*/
1289+
fun <T> registerVoiceInstructionsTriggerObserver(
1290+
observer: T
1291+
) where T : RoutesObserver, T : RouteProgressObserver {
1292+
registerRoutesObserver(observer)
1293+
registerRouteProgressObserver(observer)
1294+
}
1295+
1296+
/**
1297+
* Unsubscribes voice instructions trigger observer from all the updates it was subsribed for.
1298+
*/
1299+
fun <T> unregisterVoiceInstructionsTriggerObserver(
1300+
observer: T
1301+
) where T : RoutesObserver, T : RouteProgressObserver {
1302+
unregisterRoutesObserver(observer)
1303+
unregisterRouteProgressObserver(observer)
1304+
}
1305+
12861306
/**
12871307
* Unregisters [RoutesObserver].
12881308
*/

libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import com.mapbox.navigation.core.trip.session.NativeSetRouteValue
3939
import com.mapbox.navigation.core.trip.session.NavigationSession
4040
import com.mapbox.navigation.core.trip.session.OffRouteObserver
4141
import com.mapbox.navigation.core.trip.session.RoadObjectsOnRouteObserver
42+
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
4243
import com.mapbox.navigation.core.trip.session.TripSessionState
4344
import com.mapbox.navigation.core.trip.session.TripSessionStateObserver
4445
import com.mapbox.navigation.core.trip.session.createSetRouteResult
@@ -2019,6 +2020,42 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() {
20192020
}
20202021
}
20212022

2023+
@Test
2024+
fun registerVoiceInstructionsTriggerObserver() {
2025+
val observer = TestVoiceInstructionsTriggerObserver()
2026+
createMapboxNavigation()
2027+
2028+
mapboxNavigation.registerVoiceInstructionsTriggerObserver(observer)
2029+
2030+
verify(exactly = 1) {
2031+
directionsSession.registerRoutesObserver(observer)
2032+
tripSession.registerRouteProgressObserver(observer)
2033+
}
2034+
}
2035+
2036+
@Test
2037+
fun unregisterVoiceInstructionsTriggerObserver() {
2038+
val observer = TestVoiceInstructionsTriggerObserver()
2039+
createMapboxNavigation()
2040+
mapboxNavigation.registerVoiceInstructionsTriggerObserver(observer)
2041+
2042+
mapboxNavigation.unregisterVoiceInstructionsTriggerObserver(observer)
2043+
2044+
verify(exactly = 1) {
2045+
directionsSession.unregisterRoutesObserver(observer)
2046+
tripSession.unregisterRouteProgressObserver(observer)
2047+
}
2048+
}
2049+
2050+
private class TestVoiceInstructionsTriggerObserver : RoutesObserver, RouteProgressObserver {
2051+
2052+
override fun onRouteProgressChanged(routeProgress: RouteProgress) {
2053+
}
2054+
2055+
override fun onRoutesChanged(result: RoutesUpdatedResult) {
2056+
}
2057+
}
2058+
20222059
private fun alternativeWithId(mockId: String): RouteAlternative {
20232060
val mockedRoute = mockk<RouteInterface> {
20242061
every { routeId } returns mockId
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package com.mapbox.navigation.utils.internal
22

3+
import java.util.concurrent.TimeUnit
4+
35
interface Time {
46
fun nanoTime(): Long
57
fun millis(): Long
8+
fun seconds(): Long
69

710
object SystemImpl : Time {
811
override fun nanoTime(): Long = System.nanoTime()
912

1013
override fun millis(): Long = System.currentTimeMillis()
14+
15+
override fun seconds(): Long = TimeUnit.MILLISECONDS.toSeconds(millis())
1116
}
1217
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.mapbox.navigation.utils.internal
2+
3+
import org.junit.Assert.assertTrue
4+
import org.junit.Test
5+
import kotlin.math.abs
6+
7+
class TimeTest {
8+
9+
@Test
10+
fun seconds() {
11+
val tolerance = 1
12+
val diff = abs(System.currentTimeMillis() / 1000 - Time.SystemImpl.seconds())
13+
assertTrue(diff < tolerance)
14+
}
15+
16+
@Test
17+
fun millis() {
18+
val tolerance = 100
19+
val diff = abs(System.currentTimeMillis() - Time.SystemImpl.millis())
20+
assertTrue(diff < tolerance)
21+
}
22+
23+
@Test
24+
fun nanoTime() {
25+
val tolerance = 100000000
26+
val diff = abs(System.nanoTime() - Time.SystemImpl.nanoTime())
27+
assertTrue(diff < tolerance)
28+
}
29+
}

libnavui-voice/api/current.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ package com.mapbox.navigation.ui.voice.api {
6464
method public void cancel();
6565
method public void clean(com.mapbox.navigation.ui.voice.model.SpeechAnnouncement announcement);
6666
method public void generate(com.mapbox.api.directions.v5.models.VoiceInstructions voiceInstruction, com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer<com.mapbox.bindgen.Expected<com.mapbox.navigation.ui.voice.model.SpeechError,com.mapbox.navigation.ui.voice.model.SpeechValue>> consumer);
67+
method public void generatePredownloaded(com.mapbox.api.directions.v5.models.VoiceInstructions voiceInstruction, com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer<com.mapbox.bindgen.Expected<com.mapbox.navigation.ui.voice.model.SpeechError,com.mapbox.navigation.ui.voice.model.SpeechValue>> consumer);
6768
}
6869

6970
@UiThread public final class MapboxVoiceInstructionsPlayer {
@@ -84,6 +85,20 @@ package com.mapbox.navigation.ui.voice.api {
8485
method @kotlin.jvm.Throws(exceptionClasses=IllegalArgumentException::class) public void volume(com.mapbox.navigation.ui.voice.model.SpeechVolume state) throws java.lang.IllegalArgumentException;
8586
}
8687

88+
public final class VoiceInstructionsDownloadTrigger implements com.mapbox.navigation.core.trip.session.RouteProgressObserver com.mapbox.navigation.core.directions.session.RoutesObserver {
89+
ctor public VoiceInstructionsDownloadTrigger(com.mapbox.navigation.ui.voice.api.MapboxSpeechApi speechApi);
90+
ctor public VoiceInstructionsDownloadTrigger(int observableTime, double timePercentageToTriggerAfter, com.mapbox.navigation.ui.voice.api.MapboxSpeechApi speechApi);
91+
method public void destroy();
92+
method public void onRouteProgressChanged(com.mapbox.navigation.base.trip.model.RouteProgress routeProgress);
93+
method public void onRoutesChanged(com.mapbox.navigation.core.directions.session.RoutesUpdatedResult result);
94+
field public static final com.mapbox.navigation.ui.voice.api.VoiceInstructionsDownloadTrigger.Companion Companion;
95+
field public static final int DEFAULT_OBSERVABLE_TIME_SECONDS = 180; // 0xb4
96+
field public static final double DEFAULT_TIME_PERCENTAGE_TO_TRIGGER_AFTER = 0.5;
97+
}
98+
99+
public static final class VoiceInstructionsDownloadTrigger.Companion {
100+
}
101+
87102
public abstract sealed class VoiceInstructionsPlayerAttributes {
88103
method protected abstract kotlin.jvm.functions.Function1<android.media.AudioFocusRequest.Builder,kotlin.Unit> configureAudioFocusRequestBuilder(com.mapbox.navigation.ui.voice.model.AudioFocusOwner owner);
89104
method protected abstract kotlin.jvm.functions.Function1<android.media.MediaPlayer,kotlin.Unit> configureMediaPlayer();

libnavui-voice/src/main/java/com/mapbox/navigation/ui/voice/api/MapboxAudioGuidance.kt

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ internal constructor(
4343

4444
private var dataStoreOwner: NavigationDataStoreOwner? = null
4545
private var configOwner: NavigationConfigOwner? = null
46+
private var audioGuidanceVoice: MapboxAudioGuidanceVoice? = null
4647
private var mutedStateFlow = MutableStateFlow(false)
4748
private val internalStateFlow = MutableStateFlow(MapboxAudioGuidanceState())
4849
private val scope = CoroutineScope(SupervisorJob() + dispatcher)
@@ -74,6 +75,7 @@ internal constructor(
7475
*/
7576
override fun onDetached(mapboxNavigation: MapboxNavigation) {
7677
mapboxVoiceInstructions.unregisterObservers(mapboxNavigation)
78+
audioGuidanceVoice?.destroy()
7779
job?.cancel()
7880
job = null
7981
}
@@ -164,15 +166,24 @@ internal constructor(
164166
}
165167
}
166168

167-
private fun MapboxNavigation.audioGuidanceVoice(): Flow<MapboxAudioGuidanceVoice> =
168-
combine(
169+
private fun MapboxNavigation.audioGuidanceVoice(): Flow<MapboxAudioGuidanceVoice> {
170+
var trigger: VoiceInstructionsDownloadTrigger? = null
171+
return combine(
169172
mapboxVoiceInstructions.voiceLanguage(),
170173
configOwner!!.language(),
171174
) { voiceLanguage, deviceLanguage -> voiceLanguage ?: deviceLanguage }
172175
.distinctUntilChanged()
173176
.map { language ->
174-
audioGuidanceServices.mapboxAudioGuidanceVoice(this, language)
177+
audioGuidanceVoice?.destroy()
178+
trigger?.let { unregisterVoiceInstructionsTriggerObserver(it) }
179+
audioGuidanceServices.mapboxAudioGuidanceVoice(this, language).also {
180+
audioGuidanceVoice = it
181+
trigger = VoiceInstructionsDownloadTrigger(it.mapboxSpeechApi).also {
182+
registerVoiceInstructionsTriggerObserver(it)
183+
}
184+
}
175185
}
186+
}
176187

177188
private suspend fun restoreMutedState() {
178189
dataStoreOwner?.apply {

libnavui-voice/src/main/java/com/mapbox/navigation/ui/voice/api/MapboxSpeechApi.kt

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import com.mapbox.api.directions.v5.models.VoiceInstructions
55
import com.mapbox.bindgen.Expected
66
import com.mapbox.bindgen.ExpectedFactory
7+
import com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver
78
import com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer
89
import com.mapbox.navigation.ui.voice.model.SpeechAnnouncement
910
import com.mapbox.navigation.ui.voice.model.SpeechError
@@ -40,6 +41,9 @@ class MapboxSpeechApi @JvmOverloads constructor(
4041
* Given [VoiceInstructions] the method will try to generate the
4142
* voice instruction [SpeechAnnouncement] including the synthesized speech mp3 file
4243
* from Mapbox's API Voice.
44+
* NOTE: this method will try downloading an mp3 file from server. If you use voice instructions
45+
* predownloading (see [VoiceInstructionsDownloadTrigger]), invoke [generatePredownloaded]
46+
* instead of this method in your [VoiceInstructionsObserver].
4347
* @param voiceInstruction VoiceInstructions object representing [VoiceInstructions]
4448
* @param consumer is a [SpeechValue] including the announcement to be played when the
4549
* announcement is ready or a [SpeechError] including the error information and a fallback
@@ -51,7 +55,30 @@ class MapboxSpeechApi @JvmOverloads constructor(
5155
consumer: MapboxNavigationConsumer<Expected<SpeechError, SpeechValue>>
5256
) {
5357
mainJobController.scope.launch {
54-
retrieveVoiceFile(voiceInstruction, consumer)
58+
retrieveVoiceFile(voiceInstruction, consumer, onlyCache = false)
59+
}
60+
}
61+
62+
/**
63+
* Given [VoiceInstructions] the method will try to generate the
64+
* voice instruction [SpeechAnnouncement] including the synthesized speech mp3 file
65+
* from Mapbox's API Voice.
66+
* NOTE: this method will NOT try downloading an mp3 file from server. It will either use
67+
* an already predownloaded file or an onboard speech synthesizer. Only invoke this method
68+
* if you use voice instructions predownloading (see [VoiceInstructionsDownloadTrigger]),
69+
* otherwise invoke [generatePredownloaded] in your [VoiceInstructionsObserver].
70+
* @param voiceInstruction VoiceInstructions object representing [VoiceInstructions]
71+
* @param consumer is a [SpeechValue] including the announcement to be played when the
72+
* announcement is ready or a [SpeechError] including the error information and a fallback
73+
* with the raw announcement (without file) that can be played with a text-to-speech engine.
74+
* @see [cancel]
75+
*/
76+
fun generatePredownloaded(
77+
voiceInstruction: VoiceInstructions,
78+
consumer: MapboxNavigationConsumer<Expected<SpeechError, SpeechValue>>
79+
) {
80+
mainJobController.scope.launch {
81+
retrieveVoiceFile(voiceInstruction, consumer, onlyCache = true)
5582
}
5683
}
5784

@@ -75,12 +102,23 @@ class MapboxSpeechApi @JvmOverloads constructor(
75102
voiceAPI.clean(announcement)
76103
}
77104

105+
internal fun predownload(instructions: List<VoiceInstructions>) {
106+
mainJobController.scope.launch {
107+
voiceAPI.predownload(instructions)
108+
}
109+
}
110+
111+
internal fun destroy() {
112+
voiceAPI.destroy()
113+
}
114+
78115
@Throws(IllegalStateException::class)
79116
private suspend fun retrieveVoiceFile(
80117
voiceInstruction: VoiceInstructions,
81-
consumer: MapboxNavigationConsumer<Expected<SpeechError, SpeechValue>>
118+
consumer: MapboxNavigationConsumer<Expected<SpeechError, SpeechValue>>,
119+
onlyCache: Boolean
82120
) {
83-
when (val result = voiceAPI.retrieveVoiceFile(voiceInstruction)) {
121+
when (val result = voiceAPI.retrieveVoiceFile(voiceInstruction, onlyCache)) {
84122
is VoiceState.VoiceFile -> {
85123
val announcement = voiceInstruction.announcement()
86124
val ssmlAnnouncement = voiceInstruction.ssmlAnnouncement()

0 commit comments

Comments
 (0)