Skip to content

Commit e330118

Browse files
committed
NAVAND-552: predownload voice instructions
1 parent 868e038 commit e330118

File tree

24 files changed

+1363
-331
lines changed

24 files changed

+1363
-331
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ Mapbox welcomes participation and contributions from everyone.
3737
- Fixed an issue with `NavigationView` that caused road label position to not update in some cases. [#6531](https://github.com/mapbox/mapbox-navigation-android/pull/6531)
3838
- Fixed an issue where `DirectionsResponse#waypoints` list was cleared after a successful non-EV route refresh. [#6539](https://github.com/mapbox/mapbox-navigation-android/pull/6539)
3939
- Fixed an issue with EV route refresh failing in cases where EV data updates are not provided. Now, the initial parameters from a route request will be used as a fallback. [#6534](https://github.com/mapbox/mapbox-navigation-android/pull/6534)
40+
- Optimized voice instructions downloading.
41+
- Fixed an issue where voice instruction was being played too late with low connectivity.
4042

4143
## Mapbox Navigation SDK 2.10.0-alpha.1 - 28 October, 2022
4244
### Changelog

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ class MapboxNavigationActivity : AppCompatActivity() {
456456
mapboxNavigation.onDestroy()
457457
maneuverApi.cancel()
458458
speechAPI.cancel()
459+
speechAPI.destroy()
459460
voiceInstructionsPlayer.shutdown()
460461
}
461462

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ class MapboxVoiceActivity : AppCompatActivity(), OnMapLongClickListener {
410410
mapboxReplayer.finish()
411411
mapboxNavigation.onDestroy()
412412
speechApi.cancel()
413+
speechApi.destroy()
413414
voiceInstructionsPlayer.shutdown()
414415
}
415416

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ package com.mapbox.navigation.ui.voice.api {
5555
ctor public MapboxSpeechApi(android.content.Context context, String accessToken, String language);
5656
method public void cancel();
5757
method public void clean(com.mapbox.navigation.ui.voice.model.SpeechAnnouncement announcement);
58+
method public void destroy();
5859
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);
5960
}
6061

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ class MapboxSpeechApi @JvmOverloads constructor(
7575
voiceAPI.clean(announcement)
7676
}
7777

78+
/**
79+
* The method stops all work related to pre-downloading voice instructions and unregisters
80+
* all related callbacks. It should be invoked from `Activity#onDestroy`.
81+
*/
82+
fun destroy() {
83+
voiceAPI.destroy()
84+
}
85+
7886
@Throws(IllegalStateException::class)
7987
private suspend fun retrieveVoiceFile(
8088
voiceInstruction: VoiceInstructions,

libnavui-voice/src/main/java/com/mapbox/navigation/ui/voice/api/MapboxSpeechProvider.kt renamed to libnavui-voice/src/main/java/com/mapbox/navigation/ui/voice/api/MapboxSpeechLoader.kt

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
package com.mapbox.navigation.ui.voice.api
66

77
import android.net.Uri
8+
import com.mapbox.api.directions.v5.models.VoiceInstructions
89
import com.mapbox.bindgen.Expected
910
import com.mapbox.bindgen.ExpectedFactory.createError
1011
import com.mapbox.bindgen.ExpectedFactory.createValue
12+
import com.mapbox.common.NetworkRestriction
1113
import com.mapbox.common.ResourceLoadError
14+
import com.mapbox.common.ResourceLoadFlags
1215
import com.mapbox.common.ResourceLoadResult
1316
import com.mapbox.common.ResourceLoadStatus
1417
import com.mapbox.navigation.base.internal.accounts.UrlSkuTokenProvider
@@ -17,28 +20,88 @@ import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoader
1720
import com.mapbox.navigation.ui.utils.internal.resource.load
1821
import com.mapbox.navigation.ui.voice.model.TypeAndAnnouncement
1922
import com.mapbox.navigation.ui.voice.options.MapboxSpeechApiOptions
23+
import com.mapbox.navigation.utils.internal.InternalJobControlFactory
24+
import com.mapbox.navigation.utils.internal.logE
25+
import kotlinx.coroutines.cancel
26+
import kotlinx.coroutines.cancelChildren
27+
import kotlinx.coroutines.launch
2028
import java.net.MalformedURLException
2129
import java.net.URL
2230

23-
internal class MapboxSpeechProvider(
31+
internal class MapboxSpeechLoader(
2432
private val accessToken: String,
2533
private val language: String,
2634
private val urlSkuTokenProvider: UrlSkuTokenProvider,
2735
private val options: MapboxSpeechApiOptions,
28-
private val resourceLoader: ResourceLoader
29-
) {
36+
private val resourceLoader: ResourceLoader,
37+
) : VoiceInstructionsDownloadTriggerObserver {
3038

31-
suspend fun load(typeAndAnnouncement: TypeAndAnnouncement): Expected<Throwable, ByteArray> {
39+
private val currentRequests = mutableSetOf<Long>()
40+
private val downloadedInstructions = mutableSetOf<TypeAndAnnouncement>()
41+
private val downloadedInstructionsLock = Any()
42+
private val defaultScope = InternalJobControlFactory.createDefaultScopeJobControl().scope
43+
44+
suspend fun load(voiceInstruction: VoiceInstructions): Expected<Throwable, ByteArray> {
3245
return runCatching {
33-
val url = instructionUrl(typeAndAnnouncement.announcement, typeAndAnnouncement.type)
34-
val response = resourceLoader.load(url)
46+
val typeAndAnnouncement = VoiceInstructionsParser.parse(voiceInstruction)
47+
.getValueOrElse { throw it }
48+
val request = createRequest(typeAndAnnouncement).apply {
49+
networkRestriction = NetworkRestriction.DISALLOW_ALL
50+
}
51+
val response = resourceLoader.load(request)
3552
return processResponse(response)
3653
}.getOrElse {
3754
createError(it)
3855
}
3956
}
4057

41-
private suspend fun ResourceLoader.load(url: String) = load(ResourceLoadRequest(url))
58+
override fun trigger(voiceInstructions: List<VoiceInstructions>) {
59+
defaultScope.launch {
60+
voiceInstructions.forEach { voiceInstruction ->
61+
val typeAndAnnouncement = VoiceInstructionsParser.parse(voiceInstruction).value
62+
if (typeAndAnnouncement != null && !hasTypeAndAnnouncement(typeAndAnnouncement)) {
63+
predownload(typeAndAnnouncement)
64+
}
65+
}
66+
}
67+
}
68+
69+
fun cancel() {
70+
defaultScope.cancel()
71+
currentRequests.forEach { resourceLoader.cancel(it) }
72+
}
73+
74+
private fun hasTypeAndAnnouncement(typeAndAnnouncement: TypeAndAnnouncement): Boolean {
75+
synchronized(downloadedInstructionsLock) {
76+
return typeAndAnnouncement in downloadedInstructions
77+
}
78+
}
79+
80+
private fun predownload(typeAndAnnouncement: TypeAndAnnouncement) {
81+
try {
82+
val request = createRequest(typeAndAnnouncement)
83+
var id: Long? = null
84+
id = resourceLoader.load(request) { result ->
85+
id?.let { currentRequests.remove(it) }
86+
// tilestore thread
87+
if (result.isValue) {
88+
synchronized(downloadedInstructionsLock) {
89+
downloadedInstructions.add(typeAndAnnouncement)
90+
}
91+
}
92+
}
93+
currentRequests.add(id)
94+
} catch (ex: Throwable) {
95+
logE("Failed to download instruction '$typeAndAnnouncement': ${ex.localizedMessage}")
96+
}
97+
}
98+
99+
private fun createRequest(typeAndAnnouncement: TypeAndAnnouncement): ResourceLoadRequest {
100+
val url = instructionUrl(typeAndAnnouncement.announcement, typeAndAnnouncement.type)
101+
return ResourceLoadRequest(url).apply {
102+
flags = ResourceLoadFlags.ACCEPT_EXPIRED
103+
}
104+
}
42105

43106
private fun processResponse(
44107
response: Expected<ResourceLoadError, ResourceLoadResult>

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,21 @@ import java.io.File
1212
* Implementation of [VoiceApi] allowing you to retrieve voice instructions.
1313
*/
1414
internal class MapboxVoiceApi(
15-
private val speechProvider: MapboxSpeechProvider,
15+
private val speechLoader: MapboxSpeechLoader,
1616
private val speechFileProvider: MapboxSpeechFileProvider
1717
) : VoiceApi {
1818

19+
init {
20+
VoiceInstructionsPredownloadHub.register(speechLoader)
21+
}
22+
1923
/**
2024
* Given [VoiceInstructions] the method returns a [File] wrapped inside [VoiceState]
2125
* @param voiceInstruction VoiceInstructions object representing [VoiceInstructions]
2226
*/
2327
override suspend fun retrieveVoiceFile(voiceInstruction: VoiceInstructions): VoiceState {
2428
return runCatching {
25-
val typeAndAnnouncement = VoiceInstructionsParser.parse(voiceInstruction).getOrThrow()
26-
val blob = speechProvider.load(typeAndAnnouncement).getOrThrow()
29+
val blob = speechLoader.load(voiceInstruction).getOrThrow()
2730
val file = speechFileProvider.generateVoiceFileFrom(blob.inputStream())
2831
VoiceFile(file)
2932
}.getOrElse {
@@ -48,6 +51,11 @@ internal class MapboxVoiceApi(
4851
speechFileProvider.cancel()
4952
}
5053

54+
fun destroy() {
55+
VoiceInstructionsPredownloadHub.unregister(speechLoader)
56+
speechLoader.cancel()
57+
}
58+
5159
private fun genericError(voiceInstruction: VoiceInstructions) =
5260
"Cannot load voice instructions $voiceInstruction"
5361

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

Lines changed: 15 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package com.mapbox.navigation.ui.voice.api
22

3-
import com.mapbox.api.directions.v5.models.LegStep
4-
import com.mapbox.api.directions.v5.models.RouteLeg
53
import com.mapbox.api.directions.v5.models.VoiceInstructions
64
import com.mapbox.navigation.utils.internal.logW
75

@@ -27,63 +25,42 @@ internal class TimeBasedNextVoiceInstructionsProvider(
2725
}
2826

2927
val voiceInstructions = mutableListOf<VoiceInstructions>()
30-
fillCurrentStepVoiceInstructions(
31-
currentStep,
32-
progress.stepDistanceRemaining,
33-
voiceInstructions
34-
)
3528
var cumulatedTime = progress.stepDurationRemaining
29+
val currentStepInstructions = currentStep.voiceInstructions()?.filter { instruction ->
30+
val distanceAlongGeometry = instruction.distanceAlongGeometry()
31+
distanceAlongGeometry != null
32+
&& distanceAlongGeometry <= progress.stepDistanceRemaining
33+
}
34+
if (currentStepInstructions != null) {
35+
voiceInstructions.addAll(currentStepInstructions)
36+
}
3637

37-
// fill next steps
3838
var currentStepIndex = progress.stepIndex
3939
var currentLegIndex = progress.legIndex
4040
while (cumulatedTime < observableTimeSeconds) {
41-
if (isLastStep(currentStepIndex, legSteps)) {
41+
if (currentStepIndex + 1 < (legSteps?.size ?: 0)) {
42+
currentStep = legSteps!![currentStepIndex + 1]
43+
currentStepIndex++
44+
} else {
4245
currentStepIndex = 0
43-
if (isLastLeg(currentLegIndex, legs)) {
44-
break
45-
} else {
46+
if (currentLegIndex + 1 < legs.size) {
4647
legSteps = legs[currentLegIndex + 1].steps()
4748
currentLegIndex++
4849
if (legSteps.isNullOrEmpty()) {
4950
continue
5051
} else {
5152
currentStep = legSteps.first()
5253
}
54+
} else {
55+
break
5356
}
54-
} else {
55-
currentStep = legSteps!![currentStepIndex + 1]
56-
currentStepIndex++
5757
}
5858
currentStep.voiceInstructions()?.let { voiceInstructions.addAll(it) }
5959
cumulatedTime += currentStep.duration()
6060
}
6161
return voiceInstructions
6262
}
6363

64-
private fun fillCurrentStepVoiceInstructions(
65-
currentStep: LegStep,
66-
stepDistanceRemaining: Double,
67-
voiceInstructions: MutableList<VoiceInstructions>
68-
) {
69-
val currentStepInstructions = currentStep.voiceInstructions()?.filter { instruction ->
70-
val distanceAlongGeometry = instruction.distanceAlongGeometry()
71-
distanceAlongGeometry != null &&
72-
distanceAlongGeometry <= stepDistanceRemaining
73-
}
74-
if (currentStepInstructions != null) {
75-
voiceInstructions.addAll(currentStepInstructions)
76-
}
77-
}
78-
79-
private fun isLastStep(currentStepIndex: Int, legSteps: List<LegStep>?): Boolean {
80-
return currentStepIndex + 1 >= (legSteps?.size ?: 0)
81-
}
82-
83-
private fun isLastLeg(currentLegIndex: Int, legs: List<RouteLeg>): Boolean {
84-
return currentLegIndex + 1 >= legs.size
85-
}
86-
8764
private companion object {
8865
private const val LOG_CATEGORY = "TimeBasedNextVoiceInstructionsProvider"
8966
}

0 commit comments

Comments
 (0)