Skip to content

Commit 7908c34

Browse files
committed
NAVAND-552: move caching to NavSDK side
1 parent 99ce8ff commit 7908c34

File tree

10 files changed

+505
-420
lines changed

10 files changed

+505
-420
lines changed

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

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer
99
import com.mapbox.navigation.ui.voice.model.SpeechAnnouncement
1010
import com.mapbox.navigation.ui.voice.model.SpeechError
1111
import com.mapbox.navigation.ui.voice.model.SpeechValue
12+
import com.mapbox.navigation.ui.voice.model.TypeAndAnnouncement
1213
import com.mapbox.navigation.ui.voice.model.VoiceState
1314
import com.mapbox.navigation.ui.voice.options.MapboxSpeechApiOptions
1415
import com.mapbox.navigation.utils.internal.InternalJobControlFactory
16+
import kotlinx.coroutines.cancel
1517
import kotlinx.coroutines.launch
1618
import java.util.Locale
1719

@@ -29,7 +31,12 @@ class MapboxSpeechApi @JvmOverloads constructor(
2931
private val options: MapboxSpeechApiOptions = MapboxSpeechApiOptions.Builder().build()
3032
) {
3133

34+
private val cachedFilesLock = Any()
35+
private val cachedFiles = mutableMapOf<TypeAndAnnouncement, SpeechValue>()
3236
private val mainJobController by lazy { InternalJobControlFactory.createMainScopeJobControl() }
37+
private val predownloadScope by lazy {
38+
InternalJobControlFactory.createDefaultScopeJobControl().scope
39+
}
3340
private val voiceAPI = VoiceApiProvider.retrieveMapboxVoiceApi(
3441
context,
3542
accessToken,
@@ -55,7 +62,7 @@ class MapboxSpeechApi @JvmOverloads constructor(
5562
consumer: MapboxNavigationConsumer<Expected<SpeechError, SpeechValue>>
5663
) {
5764
mainJobController.scope.launch {
58-
retrieveVoiceFile(voiceInstruction, consumer, onlyCache = false)
65+
retrieveVoiceFile(voiceInstruction, consumer)
5966
}
6067
}
6168

@@ -78,7 +85,18 @@ class MapboxSpeechApi @JvmOverloads constructor(
7885
consumer: MapboxNavigationConsumer<Expected<SpeechError, SpeechValue>>
7986
) {
8087
mainJobController.scope.launch {
81-
retrieveVoiceFile(voiceInstruction, consumer, onlyCache = true)
88+
val cachedValue = getFromCache(voiceInstruction)
89+
if (cachedValue != null) {
90+
consumer.accept(ExpectedFactory.createValue(cachedValue))
91+
} else {
92+
val fallback = getFallbackAnnouncement(voiceInstruction)
93+
val speechError = SpeechError(
94+
"No predownloaded instruction for ${voiceInstruction.announcement()}",
95+
null,
96+
fallback
97+
)
98+
consumer.accept(ExpectedFactory.createError(speechError))
99+
}
82100
}
83101
}
84102

@@ -100,25 +118,52 @@ class MapboxSpeechApi @JvmOverloads constructor(
100118
*/
101119
fun clean(announcement: SpeechAnnouncement) {
102120
voiceAPI.clean(announcement)
121+
VoiceInstructionsParser.parse(announcement).onValue {
122+
synchronized(cachedFilesLock) {
123+
val value = cachedFiles[it]
124+
// when we clear fallback announcement, there is a chance we will remove the key
125+
// from map and not remove the file itself
126+
// since for fallback SpeechAnnouncement file is null
127+
if (value?.announcement == announcement) {
128+
cachedFiles.remove(it)
129+
}
130+
}
131+
}
103132
}
104133

105134
internal fun predownload(instructions: List<VoiceInstructions>) {
106135
mainJobController.scope.launch {
107-
voiceAPI.predownload(instructions)
136+
instructions.forEach { instruction ->
137+
val typeAndAnnouncement = VoiceInstructionsParser.parse(instruction).value
138+
if (typeAndAnnouncement != null && !hasTypeAndAnnouncement(typeAndAnnouncement)) {
139+
predownloadScope.launch {
140+
retrieveVoiceFile(instruction) {
141+
it.onValue { speechValue ->
142+
synchronized(cachedFilesLock) {
143+
cachedFiles[typeAndAnnouncement] = speechValue
144+
}
145+
}
146+
}
147+
}
148+
}
149+
}
108150
}
109151
}
110152

111153
internal fun destroy() {
112-
voiceAPI.destroy()
154+
predownloadScope.cancel()
155+
synchronized(cachedFilesLock) {
156+
val announcements = cachedFiles.map { it.value.announcement }
157+
announcements.forEach { clean(it) }
158+
}
113159
}
114160

115161
@Throws(IllegalStateException::class)
116162
private suspend fun retrieveVoiceFile(
117163
voiceInstruction: VoiceInstructions,
118164
consumer: MapboxNavigationConsumer<Expected<SpeechError, SpeechValue>>,
119-
onlyCache: Boolean
120165
) {
121-
when (val result = voiceAPI.retrieveVoiceFile(voiceInstruction, onlyCache)) {
166+
when (val result = voiceAPI.retrieveVoiceFile(voiceInstruction)) {
122167
is VoiceState.VoiceFile -> {
123168
val announcement = voiceInstruction.announcement()
124169
val ssmlAnnouncement = voiceInstruction.ssmlAnnouncement()
@@ -155,4 +200,17 @@ class MapboxSpeechApi @JvmOverloads constructor(
155200
.ssmlAnnouncement(ssmlAnnouncement)
156201
.build()
157202
}
203+
204+
private fun hasTypeAndAnnouncement(typeAndAnnouncement: TypeAndAnnouncement): Boolean {
205+
synchronized(cachedFilesLock) {
206+
return typeAndAnnouncement in cachedFiles
207+
}
208+
}
209+
210+
private fun getFromCache(voiceInstruction: VoiceInstructions): SpeechValue? {
211+
val key = VoiceInstructionsParser.parse(voiceInstruction).value
212+
synchronized(cachedFilesLock) {
213+
return key?.let { cachedFiles[it] }
214+
}
215+
}
158216
}

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

Lines changed: 3 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import com.mapbox.api.directions.v5.models.VoiceInstructions
99
import com.mapbox.bindgen.Expected
1010
import com.mapbox.bindgen.ExpectedFactory.createError
1111
import com.mapbox.bindgen.ExpectedFactory.createValue
12-
import com.mapbox.common.NetworkRestriction
1312
import com.mapbox.common.ResourceLoadError
1413
import com.mapbox.common.ResourceLoadFlags
1514
import com.mapbox.common.ResourceLoadResult
@@ -20,87 +19,29 @@ import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoader
2019
import com.mapbox.navigation.ui.utils.internal.resource.load
2120
import com.mapbox.navigation.ui.voice.model.TypeAndAnnouncement
2221
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.launch
27-
import kotlinx.coroutines.suspendCancellableCoroutine
2822
import java.net.MalformedURLException
2923
import java.net.URL
30-
import kotlin.coroutines.resume
3124

32-
internal class MapboxSpeechLoader(
25+
internal class MapboxSpeechProvider(
3326
private val accessToken: String,
3427
private val language: String,
3528
private val urlSkuTokenProvider: UrlSkuTokenProvider,
3629
private val options: MapboxSpeechApiOptions,
3730
private val resourceLoader: ResourceLoader,
3831
) {
3932

40-
private val downloadedInstructions = mutableSetOf<TypeAndAnnouncement>()
41-
private val downloadedInstructionsLock = Any()
42-
private val defaultScope = InternalJobControlFactory.createDefaultScopeJobControl().scope
43-
44-
suspend fun load(
45-
voiceInstruction: VoiceInstructions,
46-
onlyCache: Boolean
47-
): Expected<Throwable, ByteArray> {
33+
suspend fun load(voiceInstruction: VoiceInstructions): Expected<Throwable, ByteArray> {
4834
return runCatching {
4935
val typeAndAnnouncement = VoiceInstructionsParser.parse(voiceInstruction)
5036
.getValueOrElse { throw it }
51-
val request = createRequest(typeAndAnnouncement).apply {
52-
if (onlyCache) {
53-
networkRestriction = NetworkRestriction.DISALLOW_ALL
54-
}
55-
}
37+
val request = createRequest(typeAndAnnouncement)
5638
val response = resourceLoader.load(request)
5739
return processResponse(response)
5840
}.getOrElse {
5941
createError(it)
6042
}
6143
}
6244

63-
fun triggerDownload(voiceInstructions: List<VoiceInstructions>) {
64-
voiceInstructions.forEach { voiceInstruction ->
65-
defaultScope.launch {
66-
val typeAndAnnouncement = VoiceInstructionsParser.parse(voiceInstruction).value
67-
if (typeAndAnnouncement != null && !hasTypeAndAnnouncement(typeAndAnnouncement)) {
68-
predownload(typeAndAnnouncement)
69-
}
70-
}
71-
}
72-
}
73-
74-
fun cancel() {
75-
defaultScope.cancel()
76-
}
77-
78-
private fun hasTypeAndAnnouncement(typeAndAnnouncement: TypeAndAnnouncement): Boolean {
79-
synchronized(downloadedInstructionsLock) {
80-
return typeAndAnnouncement in downloadedInstructions
81-
}
82-
}
83-
84-
private suspend fun predownload(typeAndAnnouncement: TypeAndAnnouncement) {
85-
try {
86-
suspendCancellableCoroutine { cont ->
87-
val request = createRequest(typeAndAnnouncement)
88-
val id = resourceLoader.load(request) { result ->
89-
// tilestore thread
90-
if (result.isValue) {
91-
synchronized(downloadedInstructionsLock) {
92-
downloadedInstructions.add(typeAndAnnouncement)
93-
}
94-
}
95-
cont.resume(Unit)
96-
}
97-
cont.invokeOnCancellation { resourceLoader.cancel(id) }
98-
}
99-
} catch (ex: Throwable) {
100-
logE("Failed to download instruction '$typeAndAnnouncement': ${ex.localizedMessage}")
101-
}
102-
}
103-
10445
private fun createRequest(typeAndAnnouncement: TypeAndAnnouncement): ResourceLoadRequest {
10546
val url = instructionUrl(typeAndAnnouncement.announcement, typeAndAnnouncement.type)
10647
return ResourceLoadRequest(url).apply {

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

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

19-
override fun predownload(instructions: List<VoiceInstructions>) {
20-
speechLoader.triggerDownload(instructions)
21-
}
22-
2319
/**
2420
* Given [VoiceInstructions] the method returns a [File] wrapped inside [VoiceState]
2521
* @param voiceInstruction VoiceInstructions object representing [VoiceInstructions]
2622
*/
27-
override suspend fun retrieveVoiceFile(
28-
voiceInstruction: VoiceInstructions,
29-
onlyCache: Boolean
30-
): VoiceState {
23+
override suspend fun retrieveVoiceFile(voiceInstruction: VoiceInstructions): VoiceState {
3124
return runCatching {
32-
val blob = speechLoader.load(voiceInstruction, onlyCache).getOrThrow()
25+
val blob = speechLoader.load(voiceInstruction).getOrThrow()
3326
val file = speechFileProvider.generateVoiceFileFrom(blob.inputStream())
3427
VoiceFile(file)
3528
}.getOrElse {
@@ -54,10 +47,6 @@ internal class MapboxVoiceApi(
5447
speechFileProvider.cancel()
5548
}
5649

57-
fun destroy() {
58-
speechLoader.cancel()
59-
}
60-
6150
private fun genericError(voiceInstruction: VoiceInstructions) =
6251
"Cannot load voice instructions $voiceInstruction"
6352

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,12 @@ import java.io.File
1010
*/
1111
internal interface VoiceApi {
1212

13-
fun predownload(instructions: List<VoiceInstructions>)
14-
1513
/**
1614
* Given [VoiceInstructions] the method returns a [File] wrapped inside [VoiceState]
1715
* @param voiceInstruction VoiceInstructions object representing [VoiceInstructions]
1816
*/
1917
suspend fun retrieveVoiceFile(
2018
voiceInstruction: VoiceInstructions,
21-
onlyCache: Boolean
2219
): VoiceState
2320

2421
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ internal object VoiceApiProvider {
1616
language: String,
1717
options: MapboxSpeechApiOptions,
1818
): MapboxVoiceApi = MapboxVoiceApi(
19-
MapboxSpeechLoader(
19+
MapboxSpeechProvider(
2020
accessToken,
2121
language,
2222
MapboxNavigationAccounts,

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,30 @@ import com.mapbox.api.directions.v5.models.VoiceInstructions
44
import com.mapbox.bindgen.Expected
55
import com.mapbox.bindgen.ExpectedFactory.createError
66
import com.mapbox.bindgen.ExpectedFactory.createValue
7+
import com.mapbox.navigation.ui.voice.model.SpeechAnnouncement
78
import com.mapbox.navigation.ui.voice.model.TypeAndAnnouncement
89

910
internal object VoiceInstructionsParser {
1011

1112
private const val SSML_TYPE = "ssml"
1213
private const val TEXT_TYPE = "text"
1314

15+
fun parse(speechAnnouncement: SpeechAnnouncement): Expected<Throwable, TypeAndAnnouncement> {
16+
return parse(speechAnnouncement.announcement, speechAnnouncement.ssmlAnnouncement)
17+
}
18+
1419
fun parse(voiceInstructions: VoiceInstructions): Expected<Throwable, TypeAndAnnouncement> {
15-
val announcement = voiceInstructions.announcement()
16-
val ssmlAnnouncement = voiceInstructions.ssmlAnnouncement()
20+
return parse(voiceInstructions.announcement(), voiceInstructions.ssmlAnnouncement())
21+
}
22+
23+
private fun parse(
24+
announcement: String?,
25+
ssmlAnnouncement: String?
26+
): Expected<Throwable, TypeAndAnnouncement> {
1727
val (type, instruction) =
18-
if (ssmlAnnouncement != null && !ssmlAnnouncement.isNullOrBlank()) {
28+
if (!ssmlAnnouncement.isNullOrBlank()) {
1929
Pair(SSML_TYPE, ssmlAnnouncement)
20-
} else if (announcement != null && !announcement.isNullOrBlank()) {
30+
} else if (!announcement.isNullOrBlank()) {
2131
Pair(TEXT_TYPE, announcement)
2232
} else {
2333
return createError(invalidInstructionsError())

0 commit comments

Comments
 (0)