Skip to content

Commit 37b8722

Browse files
authored
[Android Auto] Fix trip session state race condition (#6661)
* Fix trip session state race condition * add pr to changelog * add case where route progress is emitted while trip session state is stopped
1 parent a1cb5a2 commit 37b8722

File tree

5 files changed

+102
-20
lines changed

5 files changed

+102
-20
lines changed

android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/CarAppSyncComponent.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class CarAppSyncComponent private constructor() : MapboxNavigationObserver {
3131
private var navigationView: NavigationView? = null
3232
private var session: Session? = null
3333

34-
fun setNavigationView(navigationView: NavigationView) {
34+
fun attachNavigationView(navigationView: NavigationView) {
3535
this.navigationView = navigationView
3636
navigationView.lifecycle.addObserver(object : DefaultLifecycleObserver {
3737
override fun onCreate(owner: LifecycleOwner) {

android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/app/MainActivity.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ package com.mapbox.navigation.examples.androidauto.app
33

44
import android.os.Bundle
55
import androidx.appcompat.app.AppCompatActivity
6-
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
76
import com.mapbox.navigation.examples.androidauto.CarAppSyncComponent
87
import com.mapbox.navigation.examples.androidauto.databinding.MapboxActivityNavigationViewBinding
98

10-
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
119
class MainActivity : AppCompatActivity() {
1210
private lateinit var binding: MapboxActivityNavigationViewBinding
1311

@@ -20,6 +18,6 @@ class MainActivity : AppCompatActivity() {
2018
// This allows to simulate your location
2119
// binding.navigationView.api.routeReplayEnabled(true)
2220

23-
CarAppSyncComponent.getInstance().setNavigationView(binding.navigationView)
21+
CarAppSyncComponent.getInstance().attachNavigationView(binding.navigationView)
2422
}
2523
}

libnavui-androidauto/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ Mapbox welcomes participation and contributions from everyone.
1515
- Renamed `RoadLabelRenderer` to `CarRoadLabelRenderer`. [#6588](https://github.com/mapbox/mapbox-navigation-android/pull/6588)
1616
- Made `ActiveGuidanceScreen` internal in favor of `MapboxScreen.ACTIVE_GUIDANCE`. [#6588](https://github.com/mapbox/mapbox-navigation-android/pull/6588)
1717
- Made `NeedsLocationPermissionsScreen` internal in favor of `MapboxScreen.NEEDS_LOCATION_PERMISSION`. [#6588](https://github.com/mapbox/mapbox-navigation-android/pull/6588)
18-
18+
- Fixed race condition where `NavigationManager.updateTrip` can be called after `navigationEnded` is called. This is a likely cause to a crash seen. [#6661](https://github.com/mapbox/mapbox-navigation-android/pull/6661)
19+
1920
## androidauto-v0.16.0 - 04 November, 2022
2021
### Changelog
2122
[Changes between 0.15.0 and 0.16.0](https://github.com/mapbox/mapbox-navigation-android/compare/androidauto-v0.15.0...androidauto-v0.16.0)

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ class MapboxCarNavigationManager internal constructor(
3737
private val routeProgressObserver = RouteProgressObserver { routeProgress ->
3838
val maneuverApi = maneuverApi ?: return@RouteProgressObserver
3939
val trip = CarManeuverMapper.from(routeProgress, maneuverApi)
40-
navigationManager.updateTrip(trip)
40+
if (mapboxNavigation?.getTripSessionState() == TripSessionState.STARTED) {
41+
navigationManager.updateTrip(trip)
42+
}
4143
}
4244

4345
private val tripSessionStateObserver = TripSessionStateObserver { tripSessionState ->
@@ -85,15 +87,11 @@ class MapboxCarNavigationManager internal constructor(
8587
logAndroidAuto("MapboxNavigationManager onDetached")
8688
this.mapboxNavigation = null
8789
carTelemetry.onDetached(mapboxNavigation)
88-
mapboxNavigation.unregisterTripSessionStateObserver(tripSessionStateObserver)
8990
mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
91+
mapboxNavigation.unregisterTripSessionStateObserver(tripSessionStateObserver)
9092

91-
// clearNavigationManagerCallback() can't be called during active navigation.
92-
// However, this callback lets AA stop the trip session which we don't want it to do if
93-
// the user simply exited AA but is still navigating via the phone app. Since
94-
// there is only one instance of MapboxNavigation allowing AA to stop the trip
95-
// session when the MainCarSession is inactive but possibly the phone app. is
96-
// actively navigating would cause unpredictable side effects.
93+
// Tell android auto navigation stopped. Navigation can continue with mapbox navigation
94+
// on another device.
9795
navigationManager.navigationEnded()
9896
navigationManager.clearNavigationManagerCallback()
9997
}

libnavui-androidauto/src/test/java/com/mapbox/androidauto/navigation/MapboxCarNavigationManagerTest.kt

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ class MapboxCarNavigationManagerTest {
4545
private val navigationManagerCallbackSlot = slot<NavigationManagerCallback>()
4646
private val navigationManager: NavigationManager = mockk(relaxed = true) {
4747
every { setNavigationManagerCallback(capture(navigationManagerCallbackSlot)) } just Runs
48+
every { navigationStarted() } answers {
49+
every { clearNavigationManagerCallback() } throws IllegalStateException()
50+
every { updateTrip(any()) } just Runs
51+
}
52+
every { navigationEnded() } answers {
53+
every { clearNavigationManagerCallback() } just Runs
54+
every { updateTrip(any()) } throws IllegalStateException()
55+
}
4856
}
4957
private val carContext: CarContext = mockk {
5058
every { getCarService(NavigationManager::class.java) } returns navigationManager
@@ -138,18 +146,20 @@ class MapboxCarNavigationManagerTest {
138146

139147
@Test
140148
fun `RouteProgress should trigger updateTrip`() {
141-
val mapboxNavigation: MapboxNavigation = mockk(relaxed = true)
142-
val progressObserverSlot = slot<RouteProgressObserver>()
143-
every {
144-
mapboxNavigation.registerRouteProgressObserver(capture(progressObserverSlot))
145-
} just Runs
146-
sut.onAttached(mapboxNavigation)
149+
val tripSessionStateSlot = mutableListOf<TripSessionStateObserver>()
150+
val routeProgressObserverSlot = mutableListOf<RouteProgressObserver>()
151+
val mapboxNavigation: MapboxNavigation = mapboxNavigationMock(
152+
tripSessionStateSlot,
153+
routeProgressObserverSlot
154+
)
147155

156+
sut.onAttached(mapboxNavigation)
157+
mapboxNavigation.startTripSession()
148158
val routeProgress = mockk<RouteProgress> {
149159
every { durationRemaining } returns 100.0
150160
every { distanceRemaining } returns 500.0f
151161
}
152-
progressObserverSlot.captured.onRouteProgressChanged(routeProgress)
162+
routeProgressObserverSlot.forEach { it.onRouteProgressChanged(routeProgress) }
153163

154164
verify { navigationManager.updateTrip(any()) }
155165
}
@@ -192,4 +202,79 @@ class MapboxCarNavigationManagerTest {
192202
assertEquals(1, resultsSlot.size)
193203
assertTrue(resultsSlot[0])
194204
}
205+
206+
@Test
207+
fun `updateTrip should not be called while detaching MapboxNavigation`() = coroutineRule.runBlockingTest {
208+
val tripSessionStateSlot = mutableListOf<TripSessionStateObserver>()
209+
val routeProgressObserverSlot = mutableListOf<RouteProgressObserver>()
210+
val mapboxNavigation: MapboxNavigation = mapboxNavigationMock(
211+
tripSessionStateSlot,
212+
routeProgressObserverSlot
213+
)
214+
215+
mapboxNavigation.startTripSession()
216+
sut.onAttached(mapboxNavigation)
217+
routeProgressObserverSlot.forEach { it.onRouteProgressChanged(mockk()) }
218+
sut.onDetached(mapboxNavigation)
219+
220+
verify(exactly = 1) { navigationManager.updateTrip(any()) }
221+
}
222+
223+
@Test
224+
fun `updateTrip will not happen when MapboxNavigation emits progress while stopped`() = coroutineRule.runBlockingTest {
225+
val tripSessionStateSlot = mutableListOf<TripSessionStateObserver>()
226+
val routeProgressObserverSlot = mutableListOf<RouteProgressObserver>()
227+
val mapboxNavigation: MapboxNavigation = mapboxNavigationMock(
228+
tripSessionStateSlot,
229+
routeProgressObserverSlot
230+
)
231+
232+
mapboxNavigation.startTripSession()
233+
sut.onAttached(mapboxNavigation)
234+
routeProgressObserverSlot.forEach { it.onRouteProgressChanged(mockk()) }
235+
mapboxNavigation.stopTripSession()
236+
routeProgressObserverSlot.forEach { it.onRouteProgressChanged(mockk()) }
237+
sut.onDetached(mapboxNavigation)
238+
239+
verify(exactly = 1) { navigationManager.updateTrip(any()) }
240+
}
241+
242+
private fun mapboxNavigationMock(
243+
tripSessionStateSlot: MutableList<TripSessionStateObserver>,
244+
routeProgressObserverSlot: MutableList<RouteProgressObserver>
245+
): MapboxNavigation {
246+
val mapboxNavigation: MapboxNavigation = mockk(relaxed = true) {
247+
every { registerTripSessionStateObserver(any()) } answers {
248+
tripSessionStateSlot.add(firstArg())
249+
firstArg<TripSessionStateObserver>().onSessionStateChanged(getTripSessionState())
250+
}
251+
every { unregisterTripSessionStateObserver(any()) } answers {
252+
tripSessionStateSlot.remove(firstArg())
253+
}
254+
every { registerRouteProgressObserver(any()) } answers {
255+
routeProgressObserverSlot.add(firstArg())
256+
}
257+
every { unregisterRouteProgressObserver(any()) } answers {
258+
routeProgressObserverSlot.remove(firstArg())
259+
}
260+
every { unregisterTripSessionStateObserver(any()) } answers {
261+
// Correct ordering when unregistering the observer will make it so
262+
// routeProgressObserverSlot is empty.
263+
routeProgressObserverSlot.forEach { it.onRouteProgressChanged(mockk()) }
264+
tripSessionStateSlot.remove(firstArg())
265+
}
266+
every { startTripSession() } answers {
267+
every { getTripSessionState() } returns TripSessionState.STARTED
268+
tripSessionStateSlot.forEach { it.onSessionStateChanged(TripSessionState.STARTED) }
269+
}
270+
every { stopTripSession() } answers {
271+
every { getTripSessionState() } returns TripSessionState.STOPPED
272+
tripSessionStateSlot.forEach { it.onSessionStateChanged(TripSessionState.STOPPED) }
273+
}
274+
}
275+
tripSessionStateSlot.add { tripSessionState ->
276+
every { mapboxNavigation.getTripSessionState() } returns tripSessionState
277+
}
278+
return mapboxNavigation
279+
}
195280
}

0 commit comments

Comments
 (0)