Skip to content

Commit 4699b0f

Browse files
authored
NAVIOS-2134: Keep billing session at reroutes (#8628)
* NAVIOS-2134: Keep billing session at reroutes * Add billing session tests for setRoutes during active guidance * Move API check inside shouldStartNewBillingSession
1 parent 2c6c375 commit 4699b0f

File tree

4 files changed

+158
-6
lines changed

4 files changed

+158
-6
lines changed

Sources/MapboxNavigationCore/Billing/BillingHandler.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -413,8 +413,27 @@ final class BillingHandler: @unchecked Sendable {
413413
}
414414
}
415415

416-
func shouldStartNewBillingSession(for newRoute: Route, remainingWaypoints: [Waypoint]) -> Bool {
417-
let newRouteWaypoints = newRoute.legs.compactMap(\.destination)
416+
func shouldStartNewBillingSession(
417+
for newNavigationRoutes: NavigationRoutes,
418+
currentRouteProgress: RouteProgressState?,
419+
reason: MapboxNavigator.SetRouteReason
420+
) -> Bool {
421+
var newRouteWaypoints = newNavigationRoutes.mainRoute.route.legs.compactMap(\.destination)
422+
var remainingWaypoints = currentRouteProgress?.routeProgress.remainingWaypoints ?? []
423+
424+
// Reroutes with RerouteStrategyForMatchRoute.navigateToFinalDestination may cause
425+
// the number of waypoints to change, but should not start a new billing session
426+
// as long as the final destination stays the same
427+
let oldApi = currentRouteProgress?.routeProgress.navigationRoutes.mapboxApi
428+
let newApi = newNavigationRoutes.mapboxApi
429+
let checkOnlyLastWaypoint = reason == .reroute
430+
&& oldApi == .mapMatching
431+
&& newApi == .directions
432+
433+
if checkOnlyLastWaypoint {
434+
newRouteWaypoints = newRouteWaypoints.suffix(1)
435+
remainingWaypoints = remainingWaypoints.suffix(1)
436+
}
418437

419438
guard !newRouteWaypoints.isEmpty else {
420439
return false // Don't need to bil for routes without waypoints

Sources/MapboxNavigationCore/Navigator/MapboxNavigator.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ final class MapboxNavigator: @unchecked Sendable {
221221

222222
@MainActor
223223
func setRoutes(navigationRoutes: NavigationRoutes, startLegIndex: Int, reason: SetRouteReason) {
224-
verifyActiveGuidanceBillingSession(for: navigationRoutes)
224+
verifyActiveGuidanceBillingSession(for: navigationRoutes, reason: reason)
225225

226226
guard let sessionUUID else {
227227
Log.error(
@@ -518,7 +518,10 @@ final class MapboxNavigator: @unchecked Sendable {
518518
}
519519

520520
@MainActor
521-
private func verifyActiveGuidanceBillingSession(for navigationRoutes: NavigationRoutes) {
521+
private func verifyActiveGuidanceBillingSession(
522+
for navigationRoutes: NavigationRoutes,
523+
reason: SetRouteReason
524+
) {
522525
if let sessionUUID,
523526
let sessionType = billingHandler.sessionType(uuid: sessionUUID)
524527
{
@@ -528,8 +531,9 @@ final class MapboxNavigator: @unchecked Sendable {
528531
beginNewSession(of: .activeGuidance)
529532
case .activeGuidance:
530533
if billingHandler.shouldStartNewBillingSession(
531-
for: navigationRoutes.mainRoute.route,
532-
remainingWaypoints: currentRouteProgress?.routeProgress.remainingWaypoints ?? []
534+
for: navigationRoutes,
535+
currentRouteProgress: currentRouteProgress,
536+
reason: reason
533537
) {
534538
billingHandler.stopBillingSession(with: sessionUUID)
535539
beginNewSession(of: .activeGuidance)

Sources/MapboxNavigationCore/Navigator/NavigationRoutes.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ public struct NavigationRoutes: Equatable, @unchecked Sendable {
3333
mainRoute.nativeRoute.getRouterOrigin() == .customExternal
3434
}
3535

36+
var mapboxApi: MapboxAPI {
37+
mainRoute.nativeRoute.getMapboxAPI()
38+
}
39+
3640
init(routesData: RoutesData) async throws {
3741
let routeResponse = try await routesData.primaryRoute().convertToDirectionsRouteResponse()
3842
try self.init(routesData: routesData, routeResponse: routeResponse)

Tests/MapboxNavigationCoreTests/Navigator/MapboxNavigatorTests.swift

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ final class MapboxNavigatorTests: TestCase {
1313
var routeRefreshResult: RouteRefreshResult!
1414
let timeout: TimeInterval = 0.5
1515

16+
var routeProgressExpectation: XCTestExpectation?
17+
1618
override func setUp() async throws {
1719
try? await super.setUp()
1820

@@ -44,6 +46,14 @@ final class MapboxNavigatorTests: TestCase {
4446
movementMonitor: .init()
4547
)
4648
navigator = await MapboxNavigator(configuration: coonfiguration)
49+
50+
routeProgressExpectation = nil
51+
navigator.routeProgress
52+
.dropFirst()
53+
.sink(receiveValue: { [weak self] _ in
54+
self?.routeProgressExpectation?.fulfill()
55+
})
56+
.store(in: &subscriptions)
4757
}
4858

4959
@MainActor
@@ -258,4 +268,119 @@ final class MapboxNavigatorTests: TestCase {
258268
await navigator.updateMapMatching(status: status)
259269
await fulfillment(of: [expectation], timeout: timeout)
260270
}
271+
272+
@MainActor
273+
func testSetRoutesSimilarNewRouteKeepsSession() async {
274+
await startActiveGuidanceAndWaitForRouteProgress(with: oneLegNavigationRoutes())
275+
await setRoutesAndWaitForRouteProgress(with: oneLegNavigationRoutes(), reason: .newRoute)
276+
277+
billingServiceMock.assertEvents([.beginBillingSession(.activeGuidance)])
278+
}
279+
280+
@MainActor
281+
func testSetRoutesSimilarRerouteKeepsSession() async {
282+
await startActiveGuidanceAndWaitForRouteProgress(with: oneLegNavigationRoutes())
283+
await setRoutesAndWaitForRouteProgress(with: oneLegNavigationRoutes(), reason: .reroute)
284+
285+
billingServiceMock.assertEvents([.beginBillingSession(.activeGuidance)])
286+
}
287+
288+
@MainActor
289+
func testSetRoutesDifferentNewRouteBeginsNewSession() async {
290+
await startActiveGuidanceAndWaitForRouteProgress(with: twoLegNavigationRoutes())
291+
await setRoutesAndWaitForRouteProgress(with: oneLegNavigationRoutes(), reason: .newRoute)
292+
293+
billingServiceMock.assertEvents([
294+
.beginBillingSession(.activeGuidance),
295+
.stopBillingSession(.activeGuidance),
296+
.beginBillingSession(.activeGuidance),
297+
])
298+
}
299+
300+
@MainActor
301+
func testSetRoutesDifferentRerouteBeginsNewSession() async {
302+
await startActiveGuidanceAndWaitForRouteProgress(with: twoLegNavigationRoutes())
303+
await setRoutesAndWaitForRouteProgress(with: oneLegNavigationRoutes(), reason: .reroute)
304+
305+
billingServiceMock.assertEvents([
306+
.beginBillingSession(.activeGuidance),
307+
.stopBillingSession(.activeGuidance),
308+
.beginBillingSession(.activeGuidance),
309+
])
310+
}
311+
312+
@MainActor
313+
func testSetRoutesMapMatchingRerouteToDirectionsWithSameDestinationKeepsSession() async {
314+
// This scenario happens with RerouteStrategyForMatchRoute.navigateToFinalDestination
315+
// when there were more than one waypoint remaining before reroute
316+
await startActiveGuidanceAndWaitForRouteProgress(with: twoLegNavigationRoutes(mapboxApi: .mapMatching))
317+
await setRoutesAndWaitForRouteProgress(with: oneLegNavigationRoutes(mapboxApi: .directions), reason: .reroute)
318+
319+
billingServiceMock.assertEvents([.beginBillingSession(.activeGuidance)])
320+
}
321+
322+
// MARK: - Helpers
323+
324+
private func startActiveGuidanceAndWaitForRouteProgress(
325+
with navigationRoutes: NavigationRoutes
326+
) async {
327+
await navigator.startActiveGuidance(with: navigationRoutes, startLegIndex: 0)
328+
routeProgressExpectation = XCTestExpectation(description: "route progress after startActiveGuidance")
329+
await fulfillment(of: [routeProgressExpectation!], timeout: timeout)
330+
}
331+
332+
private func setRoutesAndWaitForRouteProgress(
333+
with navigationRoutes: NavigationRoutes,
334+
reason: MapboxNavigator.SetRouteReason
335+
) async {
336+
await navigator.setRoutes(navigationRoutes: navigationRoutes, startLegIndex: 0, reason: reason)
337+
routeProgressExpectation = XCTestExpectation(description: "route progress after setRoutes")
338+
await fulfillment(of: [routeProgressExpectation!], timeout: timeout)
339+
}
340+
341+
// Three points along California St in San Fransisco
342+
private let coordinateA = CLLocationCoordinate2D(latitude: 37.785832, longitude: -122.458148)
343+
private let coordinateB = CLLocationCoordinate2D(latitude: 37.787594, longitude: -122.444172)
344+
private let coordinateC = CLLocationCoordinate2D(latitude: 37.78927, longitude: -122.430577)
345+
346+
private func oneLegNavigationRoutes(
347+
mapboxApi: MapboxAPI = .directions
348+
) async -> NavigationRoutes {
349+
await mockNavigationRoutes(
350+
with: [mockLeg(from: coordinateA, to: coordinateC)],
351+
mapboxApi: mapboxApi
352+
)
353+
}
354+
355+
private func twoLegNavigationRoutes(
356+
mapboxApi: MapboxAPI = .directions
357+
) async -> NavigationRoutes {
358+
await mockNavigationRoutes(
359+
with: [
360+
mockLeg(from: coordinateA, to: coordinateB),
361+
mockLeg(from: coordinateB, to: coordinateC),
362+
],
363+
mapboxApi: mapboxApi
364+
)
365+
}
366+
367+
private func mockNavigationRoutes(
368+
with legs: [RouteLeg],
369+
mapboxApi: MapboxAPI = .directions
370+
) async -> NavigationRoutes {
371+
await NavigationRoutes.mock(mainRoute: .mock(
372+
route: .mock(legs: legs),
373+
nativeRoute: RouteInterfaceMock(mapboxApi: mapboxApi)
374+
))
375+
}
376+
377+
private func mockLeg(
378+
from source: CLLocationCoordinate2D,
379+
to destination: CLLocationCoordinate2D
380+
) -> RouteLeg {
381+
var leg = RouteLeg.mock()
382+
leg.source = Waypoint(coordinate: source)
383+
leg.destination = Waypoint(coordinate: destination)
384+
return leg
385+
}
261386
}

0 commit comments

Comments
 (0)