From ece0c30f0f2af1c74b3736be67a5d684a7072fd1 Mon Sep 17 00:00:00 2001 From: Kyle Madsen <> Date: Wed, 29 Apr 2020 09:05:55 -0700 Subject: [PATCH] Add ability to observe arrival independently --- .../examples/core/MultipleStopsActivity.kt | 2 +- .../navigation/core/MapboxNavigation.kt | 26 ++++++- .../core/stops/ArrivalController.kt | 34 ++++----- .../navigation/core/stops/ArrivalObserver.kt | 25 +++++++ .../core/stops/ArrivalProgressObserver.kt | 28 +++++-- .../core/stops/ArrivalProgressObserverTest.kt | 74 ++++++++++++++++--- .../internal/MapboxNativeNavigator.kt | 2 +- .../internal/MapboxNativeNavigatorImpl.kt | 2 +- 8 files changed, 149 insertions(+), 44 deletions(-) create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalObserver.kt diff --git a/examples/src/main/java/com/mapbox/navigation/examples/core/MultipleStopsActivity.kt b/examples/src/main/java/com/mapbox/navigation/examples/core/MultipleStopsActivity.kt index 13ec5c4f7d1..cad02f44124 100644 --- a/examples/src/main/java/com/mapbox/navigation/examples/core/MultipleStopsActivity.kt +++ b/examples/src/main/java/com/mapbox/navigation/examples/core/MultipleStopsActivity.kt @@ -171,7 +171,7 @@ class MultipleStopsActivity : AppCompatActivity(), OnMapReadyCallback { .build() override fun arrivalOptions(): ArrivalOptions = arrivalOptions - override fun onStopArrival(routeLegProgress: RouteLegProgress): Boolean { + override fun navigateNextRouteLeg(routeLegProgress: RouteLegProgress): Boolean { // This example shows you can use both time and distance. // Move to the next step when the distance is small Timber.i("arrival_debug legIndex=${routeLegProgress.legIndex()} distanceRemaining=${routeLegProgress.distanceRemaining()}") diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt index 1dd8a6e8bad..547eaf845ad 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt @@ -38,6 +38,7 @@ import com.mapbox.navigation.core.internal.accounts.MapboxNavigationAccounts import com.mapbox.navigation.core.internal.trip.service.TripService import com.mapbox.navigation.core.routerefresh.RouteRefreshController import com.mapbox.navigation.core.stops.ArrivalController +import com.mapbox.navigation.core.stops.ArrivalObserver import com.mapbox.navigation.core.stops.ArrivalProgressObserver import com.mapbox.navigation.core.stops.AutoArrivalController import com.mapbox.navigation.core.telemetry.MapboxNavigationTelemetry @@ -459,9 +460,9 @@ constructor( /** * Attach your own controller to determine when drivers arrived at stops via [ArrivalController] * Use [navigateNextRouteLeg] to manually move navigator to the next stop. To reset to the - * automatic arrival controller, call attachArrivalController() + * automatic arrival controller, call [attachArrivalController]. * - * @param arrivalController ArrivalObserver + * @param arrivalController [ArrivalController] */ @JvmOverloads fun attachArrivalController(arrivalController: ArrivalController = AutoArrivalController()) { arrivalProgressObserver.attach(arrivalController) @@ -476,10 +477,29 @@ constructor( tripSession.unregisterRouteProgressObserver(arrivalProgressObserver) } + /** + * Registers [ArrivalObserver]. Monitor arrival at stops and destinations. For more control + * of arrival at stops, see [attachArrivalController]. + * + * @see [unregisterArrivalObserver] + */ + fun registerArrivalObserver(arrivalObserver: ArrivalObserver) { + arrivalProgressObserver.registerObserver(arrivalObserver) + } + + /** + * Unregisters [ArrivalObserver]. + * + * @see [registerArrivalObserver] + */ + fun unregisterArrivalObserver(arrivalObserver: ArrivalObserver) { + arrivalProgressObserver.unregisterObserver(arrivalObserver) + } + /** * After arriving at a stop, this can be used to manually decide when to start * navigating to the next stop. Use the [ArrivalController] to control when to - * call navigateNextRouteLeg. + * call [navigateNextRouteLeg]. * * @return true if navigation to next stop could be started, false otherwise */ diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalController.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalController.kt index ae486b43fec..0df8cc212fe 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalController.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalController.kt @@ -1,13 +1,13 @@ package com.mapbox.navigation.core.stops import com.mapbox.navigation.base.trip.model.RouteLegProgress -import com.mapbox.navigation.base.trip.model.RouteProgress -import com.mapbox.navigation.base.trip.model.RouteProgressState import com.mapbox.navigation.core.MapboxNavigation /** - * When navigating to points of interest, you may want to control the arrival - * experience. This interface gives you options to change arrival via [MapboxNavigation.attachArrivalController] + * When navigating to points of interest, you may want to control the arrival experience. + * This interface gives you options to control arrival via [MapboxNavigation.attachArrivalController]. + * + * To observe arrival, see [ArrivalObserver] */ interface ArrivalController { @@ -18,17 +18,11 @@ interface ArrivalController { /** * Based on your [ArrivalOptions], this will be called as the next stop is approached. - * To manually navigate the next leg, return false and call [MapboxNavigation.navigateNextRouteLeg] + * To manually navigate to the next leg, return false and call [MapboxNavigation.navigateNextRouteLeg]. * - * @return true to automatically move to the next step, false to do it manually + * @return true to automatically call [MapboxNavigation.navigateNextRouteLeg], false to do it manually */ - fun onStopArrival(routeLegProgress: RouteLegProgress): Boolean - - /** - * Once the [RouteProgress.currentState] has reached [RouteProgressState.ROUTE_ARRIVED] - * for the last stop, this will be called once. - */ - fun onRouteArrival(routeProgress: RouteProgress) {} + fun navigateNextRouteLeg(routeLegProgress: RouteLegProgress): Boolean } /** @@ -38,14 +32,14 @@ interface ArrivalController { class AutoArrivalController : ArrivalController { /** - * Default arrival options + * Default arrival options. */ override fun arrivalOptions(): ArrivalOptions = ArrivalOptions.Builder().build() /** * By default this will move onto the next step. */ - override fun onStopArrival(routeLegProgress: RouteLegProgress): Boolean { + override fun navigateNextRouteLeg(routeLegProgress: RouteLegProgress): Boolean { return true } } @@ -57,13 +51,13 @@ data class ArrivalOptions( /** * While the next stop is less than [arrivalInSeconds] away, - * [ArrivalController.onStopArrival] will be called + * [ArrivalController.navigateNextRouteLeg] will be called. */ val arrivalInSeconds: Double?, /** * While the next stop is less than [arrivalInMeters] away, - * [ArrivalController.onStopArrival] will be called + * [ArrivalController.navigateNextRouteLeg] will be called. */ val arrivalInMeters: Double? ) { @@ -77,7 +71,7 @@ data class ArrivalOptions( /** * (Recommended) Use time estimation for arrival, arrival is influenced by traffic conditions. - * Arrive when the estimated time to a stop is less than or equal to this threshold + * Arrive when the estimated time to a stop is less than or equal to this threshold. */ fun arriveInSeconds(arriveInSeconds: Double?): Builder { this.arrivalInSeconds = arriveInSeconds @@ -85,7 +79,7 @@ data class ArrivalOptions( } /** - * Arrive when the estimated distance to a stop is less than or equal to this threshold + * Arrive when the estimated distance to a stop is less than or equal to this threshold. */ fun arriveInMeters(arriveInMeters: Double?): Builder { this.arrivalInMeters = arriveInMeters @@ -93,7 +87,7 @@ data class ArrivalOptions( } /** - * Build the object. If you want to disable this feature use [MapboxNavigation.removeArrivalController] + * Build the object. If you want to disable this feature use [MapboxNavigation.removeArrivalController]. */ fun build(): ArrivalOptions { check(arrivalInSeconds != null || arrivalInSeconds != null) { diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalObserver.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalObserver.kt new file mode 100644 index 00000000000..c3397ed6dfe --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalObserver.kt @@ -0,0 +1,25 @@ +package com.mapbox.navigation.core.stops + +import com.mapbox.navigation.base.trip.model.RouteLegProgress +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.base.trip.model.RouteProgressState +import com.mapbox.navigation.core.MapboxNavigation + +/** + * This gives many observers the ability to monitor the progress of arrival. + * To control the behavior of arrival, see [ArrivalController] + */ +interface ArrivalObserver { + + /** + * Once [MapboxNavigation.navigateNextRouteLeg] has been called and returns true, + * this observer will be notified of a stop arrival. + */ + fun onStopArrival(routeLegProgress: RouteLegProgress) {} + + /** + * Once the [RouteProgress.currentState] has reached [RouteProgressState.ROUTE_ARRIVED] + * for the last stop, this will be called once. + */ + fun onRouteArrival(routeProgress: RouteProgress) {} +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalProgressObserver.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalProgressObserver.kt index 7d0ac4f4b08..98a86ed9e1b 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalProgressObserver.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalProgressObserver.kt @@ -5,30 +5,46 @@ import com.mapbox.navigation.base.trip.model.RouteProgress import com.mapbox.navigation.base.trip.model.RouteProgressState import com.mapbox.navigation.core.trip.session.RouteProgressObserver import com.mapbox.navigation.core.trip.session.TripSession +import java.util.concurrent.CopyOnWriteArraySet internal class ArrivalProgressObserver( private val tripSession: TripSession ) : RouteProgressObserver { private var arrivalController: ArrivalController = AutoArrivalController() + private val arrivalObservers = CopyOnWriteArraySet() private var arrivedForRoute = false fun attach(arrivalController: ArrivalController) { this.arrivalController = arrivalController } + fun registerObserver(arrivalObserver: ArrivalObserver) { + arrivalObservers.add(arrivalObserver) + } + + fun unregisterObserver(arrivalObserver: ArrivalObserver) { + arrivalObservers.remove(arrivalObserver) + } + fun navigateNextRouteLeg(): Boolean { - val numberOfLegs = tripSession.getRouteProgress()?.route()?.legs()?.size + val routeProgress = tripSession.getRouteProgress() + val numberOfLegs = routeProgress?.route()?.legs()?.size ?: return false - val legIndex = tripSession.getRouteProgress()?.currentLegProgress()?.legIndex() + val legProgress = routeProgress.currentLegProgress() + val legIndex = legProgress?.legIndex() ?: return false val nextLegIndex = legIndex + 1 - return if (nextLegIndex < numberOfLegs) { + val nextLegStarted = if (nextLegIndex < numberOfLegs) { val navigationStatus = tripSession.updateLegIndex(nextLegIndex) return nextLegIndex == navigationStatus.legIndex } else { - true + false + } + if (nextLegStarted) { + arrivalObservers.forEach { it.onStopArrival(legProgress) } } + return nextLegStarted } override fun onRouteProgressChanged(routeProgress: RouteProgress) { @@ -65,7 +81,7 @@ internal class ArrivalProgressObserver( } private fun doOnStopArrival(routeLegProgress: RouteLegProgress) { - val moveToNextLeg = arrivalController.onStopArrival(routeLegProgress) + val moveToNextLeg = arrivalController.navigateNextRouteLeg(routeLegProgress) if (moveToNextLeg) { navigateNextRouteLeg() } @@ -73,7 +89,7 @@ internal class ArrivalProgressObserver( private fun doOnRouteArrival(routeProgress: RouteProgress) { if (!arrivedForRoute) { - arrivalController.onRouteArrival(routeProgress) + arrivalObservers.forEach { it.onRouteArrival(routeProgress) } } } } diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/stops/ArrivalProgressObserverTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/stops/ArrivalProgressObserverTest.kt index d50111765c5..30ff85ab5ef 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/stops/ArrivalProgressObserverTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/stops/ArrivalProgressObserverTest.kt @@ -17,11 +17,13 @@ import org.junit.Test internal class ArrivalProgressObserverTest { private val tripSession: TripSession = mockk() + private val arrivalObserver: ArrivalObserver = mockk() private val arrivalProgressObserver = ArrivalProgressObserver(tripSession) @Before fun setup() { every { tripSession.getRouteProgress() } returns null + arrivalProgressObserver.registerObserver(arrivalObserver) } @Test @@ -49,13 +51,13 @@ internal class ArrivalProgressObserverTest { val onStopArrivalCalls = slot() val onRouteArrivalCalls = slot() val customArrivalController: ArrivalController = mockk { - every { onStopArrival(capture(onStopArrivalCalls)) } returns false - every { onRouteArrival(capture(onRouteArrivalCalls)) } returns Unit + every { navigateNextRouteLeg(capture(onStopArrivalCalls)) } returns false every { arrivalOptions() } returns mockk { every { arrivalInSeconds } returns null every { arrivalInMeters } returns 10.0 } } + every { arrivalObserver.onRouteArrival(capture(onRouteArrivalCalls)) } returns Unit arrivalProgressObserver.attach(customArrivalController) arrivalProgressObserver.onRouteProgressChanged(mockk { @@ -79,13 +81,13 @@ internal class ArrivalProgressObserverTest { val onStopArrivalCalls = slot() val onRouteArrivalCalls = slot() val customArrivalController: ArrivalController = mockk { - every { onStopArrival(capture(onStopArrivalCalls)) } returns false - every { onRouteArrival(capture(onRouteArrivalCalls)) } returns Unit + every { navigateNextRouteLeg(capture(onStopArrivalCalls)) } returns false every { arrivalOptions() } returns mockk { every { arrivalInSeconds } returns null every { arrivalInMeters } returns 10.0 } } + every { arrivalObserver.onRouteArrival(capture(onRouteArrivalCalls)) } returns Unit arrivalProgressObserver.attach(customArrivalController) arrivalProgressObserver.onRouteProgressChanged(mockk { @@ -109,13 +111,13 @@ internal class ArrivalProgressObserverTest { val onStopArrivalCalls = slot() val onRouteArrivalCalls = mutableListOf() val customArrivalController: ArrivalController = mockk { - every { onStopArrival(capture(onStopArrivalCalls)) } returns false - every { onRouteArrival(capture(onRouteArrivalCalls)) } returns Unit + every { navigateNextRouteLeg(capture(onStopArrivalCalls)) } returns false every { arrivalOptions() } returns mockk { every { arrivalInSeconds } returns null every { arrivalInMeters } returns 10.0 } } + every { arrivalObserver.onRouteArrival(capture(onRouteArrivalCalls)) } returns Unit val routeProgress: RouteProgress = mockk { every { currentState() } returns RouteProgressState.ROUTE_ARRIVED every { route() } returns mockk { @@ -135,13 +137,14 @@ internal class ArrivalProgressObserverTest { assertEquals(1, onRouteArrivalCalls.size) assertFalse(onStopArrivalCalls.isCaptured) + verify(exactly = 1) { arrivalObserver.onRouteArrival(any()) } } @Test fun `should notify observers with time option`() { val onArrivalCalls = slot() val customArrivalController: ArrivalController = mockk { - every { onStopArrival(capture(onArrivalCalls)) } returns false + every { navigateNextRouteLeg(capture(onArrivalCalls)) } returns false every { arrivalOptions() } returns mockk { every { arrivalInSeconds } returns 5.0 every { arrivalInMeters } returns null @@ -165,7 +168,7 @@ internal class ArrivalProgressObserverTest { fun `should notify observers with distance option`() { val onArrivalCalls = slot() val customArrivalController: ArrivalController = mockk { - every { onStopArrival(capture(onArrivalCalls)) } returns false + every { navigateNextRouteLeg(capture(onArrivalCalls)) } returns false every { arrivalOptions() } returns mockk { every { arrivalInSeconds } returns null every { arrivalInMeters } returns 10.0 @@ -182,13 +185,14 @@ internal class ArrivalProgressObserverTest { }) assertEquals(onArrivalCalls.captured.distanceRemaining(), 8.0f, 0.001f) + verify(exactly = 0) { arrivalObserver.onRouteArrival(any()) } } @Test fun `should notify arrival if arrived on attach`() { val onArrivalCalls = slot() val customArrivalController: ArrivalController = mockk { - every { onStopArrival(capture(onArrivalCalls)) } returns false + every { navigateNextRouteLeg(capture(onArrivalCalls)) } returns false every { arrivalOptions() } returns mockk { every { arrivalInSeconds } returns 5.0 every { arrivalInMeters } returns null @@ -213,7 +217,7 @@ internal class ArrivalProgressObserverTest { fun `should not navigate to next stop before arrival`() { val onArrivalCalls = slot() val customArrivalController: ArrivalController = mockk { - every { onStopArrival(capture(onArrivalCalls)) } returns false + every { navigateNextRouteLeg(capture(onArrivalCalls)) } returns false every { arrivalOptions() } returns mockk { every { arrivalInMeters } returns 10.0 every { arrivalInSeconds } returns 5.0 @@ -279,7 +283,7 @@ internal class ArrivalProgressObserverTest { } every { tripSession.getRouteProgress() } returns routeProgress val customArrivalController: ArrivalController = mockk { - every { onStopArrival(any()) } returns testNavigateNextRouteLeg + every { navigateNextRouteLeg(any()) } returns testNavigateNextRouteLeg every { arrivalOptions() } returns mockk { every { arrivalInSeconds } returns 0.0 every { arrivalInMeters } returns null @@ -313,7 +317,7 @@ internal class ArrivalProgressObserverTest { } } val customArrivalController: ArrivalController = mockk { - every { onStopArrival(any()) } returns testNavigateNextRouteLeg + every { navigateNextRouteLeg(any()) } returns testNavigateNextRouteLeg every { arrivalOptions() } returns mockk { every { arrivalInSeconds } returns 0.0 every { arrivalInMeters } returns null @@ -324,4 +328,50 @@ internal class ArrivalProgressObserverTest { verify(exactly = 0) { tripSession.updateLegIndex(any()) } } + + @Test + fun `navigateNextRouteLeg should return true when route leg is updated`() { + every { tripSession.getRouteProgress() } returns mockk { + every { currentState() } returns RouteProgressState.LOCATION_TRACKING + every { route() } returns mockk { + every { legs() } returns listOf( + mockk(), mockk(), mockk() // This route has three legs + ) + every { routeIndex() } returns "0" + every { currentLegProgress() } returns mockk { + every { legIndex() } returns 1 + } + } + } + every { tripSession.updateLegIndex(2) } returns mockk { + every { legIndex } returns 2 + } + + val didNavigate = arrivalProgressObserver.navigateNextRouteLeg() + + assertTrue(didNavigate) + } + + @Test + fun `navigateNextRouteLeg should return false when route leg is not updated`() { + every { tripSession.getRouteProgress() } returns mockk { + every { currentState() } returns RouteProgressState.LOCATION_TRACKING + every { route() } returns mockk { + every { legs() } returns listOf( + mockk(), mockk(), mockk() // This route has three legs + ) + every { routeIndex() } returns "0" + every { currentLegProgress() } returns mockk { + every { legIndex() } returns 2 + } + } + } + every { tripSession.updateLegIndex(3) } returns mockk { + every { legIndex } returns 2 + } + + val didNavigate = arrivalProgressObserver.navigateNextRouteLeg() + + assertFalse(didNavigate) + } } diff --git a/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigator.kt b/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigator.kt index bdc591e8bc3..aeb625537b1 100644 --- a/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigator.kt +++ b/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigator.kt @@ -120,7 +120,7 @@ interface MapboxNativeNavigator { ): String? /** - * Follows a new route and leg of the already loaded directions. + * Follows a new leg of the already loaded directions. * Returns an initialized navigation status if no errors occurred * otherwise, it returns an invalid navigation status state. * diff --git a/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigatorImpl.kt b/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigatorImpl.kt index f29c546d87e..658413c221d 100644 --- a/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigatorImpl.kt +++ b/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigatorImpl.kt @@ -180,7 +180,7 @@ object MapboxNativeNavigatorImpl : MapboxNativeNavigator { navigator.getRouteBufferGeoJson(gridSize, bufferDilation) /** - * Follows a new route and leg of the already loaded directions. + * Follows a new leg of the already loaded directions. * Returns an initialized navigation status if no errors occurred * otherwise, it returns an invalid navigation status state. *