Skip to content

Commit be42298

Browse files
committed
NAVAND-552: predownload voice instructions
1 parent b97c2f0 commit be42298

File tree

26 files changed

+1429
-300
lines changed

26 files changed

+1429
-300
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ Mapbox welcomes participation and contributions from everyone.
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)
2828
- Fixed standalone `MapboxManeuverView` appearance when the app also integrates Drop-In UI. [#6774](https://github.com/mapbox/mapbox-navigation-android/pull/6774)
29+
- 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)
30+
- Enabled voice instructions predownloading for those who use `MapboxAudioGuidance`. [#6771](https://github.com/mapbox/mapbox-navigation-android/pull/6771)
31+
- 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)
2932

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

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.mapbox.maps.plugin.gestures.OnMapLongClickListener
2222
import com.mapbox.maps.plugin.gestures.gestures
2323
import com.mapbox.maps.plugin.locationcomponent.LocationComponentPlugin
2424
import com.mapbox.maps.plugin.locationcomponent.location
25+
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
2526
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
2627
import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
2728
import com.mapbox.navigation.base.options.NavigationOptions
@@ -60,6 +61,7 @@ import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoadRequest
6061
import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoaderFactory
6162
import com.mapbox.navigation.ui.voice.api.MapboxSpeechApi
6263
import com.mapbox.navigation.ui.voice.api.MapboxVoiceInstructionsPlayer
64+
import com.mapbox.navigation.ui.voice.api.VoiceInstructionsDownloadTrigger
6365
import com.mapbox.navigation.ui.voice.model.SpeechAnnouncement
6466
import com.mapbox.navigation.ui.voice.model.SpeechError
6567
import com.mapbox.navigation.ui.voice.model.SpeechValue
@@ -201,9 +203,14 @@ class MapboxVoiceActivity : AppCompatActivity(), OnMapLongClickListener {
201203
}
202204
}
203205

206+
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
207+
private val voiceInstructionsDownloadTrigger by lazy {
208+
VoiceInstructionsDownloadTrigger(speechApi)
209+
}
210+
204211
private val voiceInstructionsObserver =
205212
VoiceInstructionsObserver { voiceInstructions -> // The data obtained must be used to generate the synthesized speech mp3 file.
206-
speechApi.generate(
213+
speechApi.generatePredownloaded(
207214
voiceInstructions,
208215
speechCallback
209216
)
@@ -384,35 +391,41 @@ class MapboxVoiceActivity : AppCompatActivity(), OnMapLongClickListener {
384391
init()
385392
}
386393

394+
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
387395
override fun onStart() {
388396
super.onStart()
389397
if (::mapboxNavigation.isInitialized) {
390398
mapboxNavigation.registerRoutesObserver(routesObserver)
391399
mapboxNavigation.registerLocationObserver(locationObserver)
400+
mapboxNavigation.registerVoiceInstructionsTriggerObserver(voiceInstructionsDownloadTrigger)
392401
mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
393402
mapboxNavigation.registerRouteProgressObserver(replayProgressObserver)
394403
mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver)
395404
}
396405
ResourceLoaderFactory.getInstance().registerObserver(resourceLoadObserver)
397406
}
398407

408+
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
399409
override fun onStop() {
400410
super.onStop()
401411
ResourceLoaderFactory.getInstance().unregisterObserver(resourceLoadObserver)
402412
mapboxNavigation.unregisterRoutesObserver(routesObserver)
413+
mapboxNavigation.registerVoiceInstructionsTriggerObserver(voiceInstructionsDownloadTrigger)
403414
mapboxNavigation.unregisterLocationObserver(locationObserver)
404415
mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
405416
mapboxNavigation.unregisterRouteProgressObserver(replayProgressObserver)
406417
mapboxNavigation.unregisterVoiceInstructionsObserver(voiceInstructionsObserver)
407418
}
408419

420+
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
409421
override fun onDestroy() {
410422
super.onDestroy()
411423
routeLineApi.cancel()
412424
routeLineView.cancel()
413425
mapboxReplayer.finish()
414426
mapboxNavigation.onDestroy()
415427
speechApi.cancel()
428+
voiceInstructionsDownloadTrigger.destroy()
416429
voiceInstructionsPlayer.shutdown()
417430
}
418431

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 @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI 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 @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI 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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,28 @@ class MapboxNavigation @VisibleForTesting internal constructor(
12831283
}
12841284
}
12851285

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

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+
@com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI 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: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.mapbox.navigation.ui.voice.api
22

33
import androidx.annotation.VisibleForTesting
44
import com.mapbox.api.directions.v5.models.VoiceInstructions
5+
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
56
import com.mapbox.navigation.core.MapboxNavigation
67
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
78
import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver
@@ -43,6 +44,7 @@ internal constructor(
4344

4445
private var dataStoreOwner: NavigationDataStoreOwner? = null
4546
private var configOwner: NavigationConfigOwner? = null
47+
private var audioGuidanceVoice: MapboxAudioGuidanceVoice? = null
4648
private var mutedStateFlow = MutableStateFlow(false)
4749
private val internalStateFlow = MutableStateFlow(MapboxAudioGuidanceState())
4850
private val scope = CoroutineScope(SupervisorJob() + dispatcher)
@@ -74,6 +76,7 @@ internal constructor(
7476
*/
7577
override fun onDetached(mapboxNavigation: MapboxNavigation) {
7678
mapboxVoiceInstructions.unregisterObservers(mapboxNavigation)
79+
audioGuidanceVoice?.destroy()
7780
job?.cancel()
7881
job = null
7982
}
@@ -164,15 +167,25 @@ internal constructor(
164167
}
165168
}
166169

167-
private fun MapboxNavigation.audioGuidanceVoice(): Flow<MapboxAudioGuidanceVoice> =
168-
combine(
170+
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
171+
private fun MapboxNavigation.audioGuidanceVoice(): Flow<MapboxAudioGuidanceVoice> {
172+
var trigger: VoiceInstructionsDownloadTrigger? = null
173+
return combine(
169174
mapboxVoiceInstructions.voiceLanguage(),
170175
configOwner!!.language(),
171176
) { voiceLanguage, deviceLanguage -> voiceLanguage ?: deviceLanguage }
172177
.distinctUntilChanged()
173178
.map { language ->
174-
audioGuidanceServices.mapboxAudioGuidanceVoice(this, language)
179+
audioGuidanceVoice?.destroy()
180+
trigger?.let { unregisterVoiceInstructionsTriggerObserver(it) }
181+
audioGuidanceServices.mapboxAudioGuidanceVoice(this, language).also {
182+
audioGuidanceVoice = it
183+
trigger = VoiceInstructionsDownloadTrigger(it.mapboxSpeechApi).also {
184+
registerVoiceInstructionsTriggerObserver(it)
185+
}
186+
}
175187
}
188+
}
176189

177190
private suspend fun restoreMutedState() {
178191
dataStoreOwner?.apply {

0 commit comments

Comments
 (0)