Skip to content

Commit 4b0299a

Browse files
committed
feat: add interface for new navigation session detection
1 parent 8dc5bc8 commit 4b0299a

File tree

12 files changed

+276
-58
lines changed

12 files changed

+276
-58
lines changed

android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionManager.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ constructor(
7070
RoadSnappedLocationProvider.GpsAvailabilityEnhancedLocationListener? =
7171
null
7272
private var speedingListener: SpeedingListener? = null
73+
private var navigationSessionListener: Navigator.NavigationSessionListener? = null
7374
private var weakActivity: WeakReference<Activity>? = null
7475
private var navInfoObserver: Observer<NavInfo>? = null
7576
private var weakLifecycleOwner: WeakReference<LifecycleOwner>? = null
@@ -315,6 +316,10 @@ constructor(
315316
navigator.setSpeedingListener(null)
316317
speedingListener = null
317318
}
319+
if (navigationSessionListener != null) {
320+
navigator.removeNavigationSessionListener(navigationSessionListener)
321+
navigationSessionListener = null
322+
}
318323
}
319324
if (roadSnappedLocationListener != null) {
320325
disableRoadSnappedLocationUpdates()
@@ -391,6 +396,12 @@ constructor(
391396
}
392397
navigator.setSpeedingListener(speedingListener)
393398
}
399+
400+
if (navigationSessionListener == null) {
401+
navigationSessionListener =
402+
Navigator.NavigationSessionListener { navigationSessionEventApi.onNewNavigationSession {} }
403+
navigator.addNavigationSessionListener(navigationSessionListener)
404+
}
394405
}
395406

396407
/**

android/src/main/kotlin/com/google/maps/flutter/navigation/messages.g.kt

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5693,7 +5693,6 @@ class ViewEventApi(
56935693

56945694
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
56955695
interface NavigationSessionApi {
5696-
/** General. */
56975696
fun createNavigationSession(
56985697
abnormalTerminationReportingEnabled: Boolean,
56995698
behavior: TaskRemovedBehaviorDto,
@@ -5717,7 +5716,6 @@ interface NavigationSessionApi {
57175716

57185717
fun getNavSDKVersion(): String
57195718

5720-
/** Navigation. */
57215719
fun isGuidanceRunning(): Boolean
57225720

57235721
fun startGuidance()
@@ -5742,7 +5740,6 @@ interface NavigationSessionApi {
57425740

57435741
fun getCurrentRouteSegment(): RouteSegmentDto?
57445742

5745-
/** Simulation */
57465743
fun setUserLocation(location: LatLngDto)
57475744

57485745
fun removeUserLocation()
@@ -5773,15 +5770,13 @@ interface NavigationSessionApi {
57735770

57745771
fun resumeSimulation()
57755772

5776-
/** Simulation (iOS only) */
5773+
/** iOS-only method. */
57775774
fun allowBackgroundLocationUpdates(allow: Boolean)
57785775

5779-
/** Road snapped location updates. */
57805776
fun enableRoadSnappedLocationUpdates()
57815777

57825778
fun disableRoadSnappedLocationUpdates()
57835779

5784-
/** Enable Turn-by-Turn navigation events. */
57855780
fun enableTurnByTurnNavigationEvents(numNextStepsToPreview: Long?)
57865781

57875782
fun disableTurnByTurnNavigationEvents()
@@ -6810,6 +6805,26 @@ class NavigationSessionEventApi(
68106805
}
68116806
}
68126807
}
6808+
6809+
/** Navigation session event. Called when a new navigation session starts with active guidance. */
6810+
fun onNewNavigationSession(callback: (Result<Unit>) -> Unit) {
6811+
val separatedMessageChannelSuffix =
6812+
if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
6813+
val channelName =
6814+
"dev.flutter.pigeon.google_navigation_flutter.NavigationSessionEventApi.onNewNavigationSession$separatedMessageChannelSuffix"
6815+
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
6816+
channel.send(null) {
6817+
if (it is List<*>) {
6818+
if (it.size > 1) {
6819+
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
6820+
} else {
6821+
callback(Result.success(Unit))
6822+
}
6823+
} else {
6824+
callback(Result.failure(MessagesPigeonUtils.createConnectionError(channelName)))
6825+
}
6826+
}
6827+
}
68136828
}
68146829

68156830
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */

example/integration_test/t03_navigation_test.dart

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -59,22 +59,12 @@ void main() {
5959
PatrolIntegrationTester $,
6060
) async {
6161
final Completer<void> hasArrived = Completer<void>();
62+
final Completer<void> newSessionFired = Completer<void>();
6263

6364
/// Set up navigation view and controller.
6465
final GoogleNavigationViewController viewController =
6566
await startNavigationWithoutDestination($);
6667

67-
/// Set audio guidance settings.
68-
/// Cannot be verified, because native SDK lacks getter methods,
69-
/// but exercise the API for basic sanity testing
70-
final NavigationAudioGuidanceSettings settings =
71-
NavigationAudioGuidanceSettings(
72-
isBluetoothAudioEnabled: true,
73-
isVibrationEnabled: true,
74-
guidanceType: NavigationAudioGuidanceType.alertsAndGuidance,
75-
);
76-
await GoogleMapsNavigator.setAudioGuidance(settings);
77-
7868
/// Specify tolerance and navigation end coordinates.
7969
const double tolerance = 0.001;
8070
const double endLat = 68.59451829688189, endLng = 23.512277951523007;
@@ -86,8 +76,28 @@ void main() {
8676
await GoogleMapsNavigator.stopGuidance();
8777
}
8878

79+
/// Set up listener for new navigation session event.
80+
Future<void> onNewNavigationSession() async {
81+
newSessionFired.complete();
82+
83+
/// Sets audio guidance settings for the current navigation session.
84+
/// Cannot be verified, because native SDK lacks getter methods,
85+
/// but exercise the API for basic sanity testing.
86+
await GoogleMapsNavigator.setAudioGuidance(
87+
NavigationAudioGuidanceSettings(
88+
isBluetoothAudioEnabled: true,
89+
isVibrationEnabled: true,
90+
guidanceType: NavigationAudioGuidanceType.alertsAndGuidance,
91+
),
92+
);
93+
}
94+
8995
final StreamSubscription<OnArrivalEvent> onArrivalSubscription =
9096
GoogleMapsNavigator.setOnArrivalListener(onArrivalEvent);
97+
final StreamSubscription<void> onNewNavigationSessionSubscription =
98+
GoogleMapsNavigator.setOnNewNavigationSessionListener(
99+
onNewNavigationSession,
100+
);
91101

92102
/// Simulate location and test it.
93103
await setSimulatedUserLocationWithCheck(
@@ -143,11 +153,24 @@ void main() {
143153
await GoogleMapsNavigator.simulator.simulateLocationsAlongExistingRoute();
144154

145155
expect(await GoogleMapsNavigator.isGuidanceRunning(), true);
156+
157+
/// Wait for new navigation session event.
158+
await newSessionFired.future.timeout(
159+
const Duration(seconds: 30),
160+
onTimeout:
161+
() =>
162+
throw TimeoutException(
163+
'New navigation session event was not fired',
164+
),
165+
);
166+
expect(newSessionFired.isCompleted, true);
167+
146168
await hasArrived.future;
147169
expect(await GoogleMapsNavigator.isGuidanceRunning(), false);
148170

149171
// Cancel subscriptions before cleanup
150172
await onArrivalSubscription.cancel();
173+
await onNewNavigationSessionSubscription.cancel();
151174
await roadSnappedSubscription.cancel();
152175
await GoogleMapsNavigator.cleanup();
153176
});
@@ -156,24 +179,14 @@ void main() {
156179
'Test navigating to multiple destinations',
157180
(PatrolIntegrationTester $) async {
158181
final Completer<void> navigationFinished = Completer<void>();
182+
final Completer<void> newSessionFired = Completer<void>();
159183
int arrivalEventCount = 0;
160184
List<NavigationWaypoint> waypoints = <NavigationWaypoint>[];
161185

162186
/// Set up navigation view and controller.
163187
final GoogleNavigationViewController viewController =
164188
await startNavigationWithoutDestination($);
165189

166-
/// Set audio guidance settings.
167-
/// Cannot be verified, because native SDK lacks getter methods,
168-
/// but exercise the API for basic sanity testing
169-
final NavigationAudioGuidanceSettings settings =
170-
NavigationAudioGuidanceSettings(
171-
isBluetoothAudioEnabled: false,
172-
isVibrationEnabled: false,
173-
guidanceType: NavigationAudioGuidanceType.alertsOnly,
174-
);
175-
await GoogleMapsNavigator.setAudioGuidance(settings);
176-
177190
/// Specify tolerance and navigation destination coordinates.
178191
const double tolerance = 0.001;
179192
const double midLat = 68.59781164189049,
@@ -234,8 +247,28 @@ void main() {
234247
}
235248
}
236249

250+
/// Set up listener for new navigation session event.
251+
Future<void> onNewNavigationSession() async {
252+
newSessionFired.complete();
253+
254+
/// Sets audio guidance settings for the current navigation session.
255+
/// Cannot be verified, because native SDK lacks getter methods,
256+
/// but exercise the API for basic sanity testing.
257+
await GoogleMapsNavigator.setAudioGuidance(
258+
NavigationAudioGuidanceSettings(
259+
isBluetoothAudioEnabled: true,
260+
isVibrationEnabled: true,
261+
guidanceType: NavigationAudioGuidanceType.alertsAndGuidance,
262+
),
263+
);
264+
}
265+
237266
final StreamSubscription<OnArrivalEvent> onArrivalSubscription =
238267
GoogleMapsNavigator.setOnArrivalListener(onArrivalEvent);
268+
final StreamSubscription<void> onNewNavigationSessionSubscription =
269+
GoogleMapsNavigator.setOnNewNavigationSessionListener(
270+
onNewNavigationSession,
271+
);
239272

240273
/// Simulate location and test it.
241274
await setSimulatedUserLocationWithCheck(
@@ -317,11 +350,24 @@ void main() {
317350
);
318351

319352
expect(await GoogleMapsNavigator.isGuidanceRunning(), true);
353+
354+
/// Wait for new navigation session event.
355+
await newSessionFired.future.timeout(
356+
const Duration(seconds: 30),
357+
onTimeout:
358+
() =>
359+
throw TimeoutException(
360+
'New navigation session event was not fired',
361+
),
362+
);
363+
expect(newSessionFired.isCompleted, true);
364+
320365
await navigationFinished.future;
321366
expect(await GoogleMapsNavigator.isGuidanceRunning(), false);
322367

323368
// Cancel subscriptions before cleanup
324369
await onArrivalSubscription.cancel();
370+
await onNewNavigationSessionSubscription.cancel();
325371
await roadSnappedSubscription.cancel();
326372
await GoogleMapsNavigator.cleanup();
327373
},

example/lib/pages/navigation.dart

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class _NavigationPageState extends ExamplePageState<NavigationPage> {
9898
int _onRecenterButtonClickedEventCallCount = 0;
9999
int _onRemainingTimeOrDistanceChangedEventCallCount = 0;
100100
int _onNavigationUIEnabledChangedEventCallCount = 0;
101+
int _onNewNavigationSessionEventCallCount = 0;
101102

102103
bool _navigationHeaderEnabled = true;
103104
bool _navigationFooterEnabled = true;
@@ -147,6 +148,7 @@ class _NavigationPageState extends ExamplePageState<NavigationPage> {
147148
_roadSnappedLocationUpdatedSubscription;
148149
StreamSubscription<RoadSnappedRawLocationUpdatedEvent>?
149150
_roadSnappedRawLocationUpdatedSubscription;
151+
StreamSubscription<void>? _newNavigationSessionSubscription;
150152

151153
int _nextWaypointIndex = 0;
152154

@@ -379,6 +381,11 @@ class _NavigationPageState extends ExamplePageState<NavigationPage> {
379381
await GoogleMapsNavigator.setRoadSnappedRawLocationUpdatedListener(
380382
_onRoadSnappedRawLocationUpdatedEvent,
381383
);
384+
385+
_newNavigationSessionSubscription =
386+
GoogleMapsNavigator.setOnNewNavigationSessionListener(
387+
_onNewNavigationSessionEvent,
388+
);
382389
}
383390

384391
void _clearListeners() {
@@ -408,6 +415,24 @@ class _NavigationPageState extends ExamplePageState<NavigationPage> {
408415

409416
_roadSnappedRawLocationUpdatedSubscription?.cancel();
410417
_roadSnappedRawLocationUpdatedSubscription = null;
418+
419+
_newNavigationSessionSubscription?.cancel();
420+
_newNavigationSessionSubscription = null;
421+
}
422+
423+
void _onNewNavigationSessionEvent() {
424+
if (!mounted) {
425+
return;
426+
}
427+
428+
setState(() {
429+
_onNewNavigationSessionEventCallCount += 1;
430+
});
431+
432+
showMessage('New navigation session started');
433+
434+
// Set audio guidance settings for the new navigation session.
435+
unawaited(_setAudioGuidance());
411436
}
412437

413438
void _onRoadSnappedLocationUpdatedEvent(
@@ -517,6 +542,16 @@ class _NavigationPageState extends ExamplePageState<NavigationPage> {
517542
await _getInitialViewStates();
518543
}
519544

545+
Future<void> _setAudioGuidance() async {
546+
await GoogleMapsNavigator.setAudioGuidance(
547+
NavigationAudioGuidanceSettings(
548+
isBluetoothAudioEnabled: true,
549+
isVibrationEnabled: true,
550+
guidanceType: NavigationAudioGuidanceType.alertsAndGuidance,
551+
),
552+
);
553+
}
554+
520555
Future<void> _getInitialViewStates() async {
521556
assert(_navigationViewController != null);
522557
if (_navigationViewController != null) {
@@ -1445,6 +1480,14 @@ class _NavigationPageState extends ExamplePageState<NavigationPage> {
14451480
),
14461481
),
14471482
),
1483+
Card(
1484+
child: ListTile(
1485+
title: const Text('New navigation session event call count'),
1486+
trailing: Text(
1487+
_onNewNavigationSessionEventCallCount.toString(),
1488+
),
1489+
),
1490+
),
14481491
],
14491492
),
14501493
);

ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionManager.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ class GoogleMapsNavigationSessionManager: NSObject {
6464

6565
private var _numTurnByTurnNextStepsToPreview = Int64.max
6666

67+
private var _isNewNavigationSessionDetected = false
68+
6769
func getNavigator() throws -> GMSNavigator {
6870
guard let _session else { throw GoogleMapsNavigationSessionManagerError.sessionNotInitialized }
6971
guard let navigator = _session.navigator
@@ -221,6 +223,7 @@ class GoogleMapsNavigationSessionManager: NSObject {
221223

222224
func stopGuidance() throws {
223225
try getNavigator().isGuidanceActive = false
226+
_isNewNavigationSessionDetected = false
224227
}
225228

226229
func isGuidanceRunning() throws -> Bool {
@@ -247,6 +250,11 @@ class GoogleMapsNavigationSessionManager: NSObject {
247250
completion: @escaping (Result<RouteStatusDto, Error>) -> Void
248251
) {
249252
do {
253+
// Reset session detection state to allow onNewNavigationSession to fire again
254+
// This mimics Android's behavior where the event fires each time setDestinations
255+
// is called while guidance is running
256+
_isNewNavigationSessionDetected = false
257+
250258
// If the session has view attached, enable given display options.
251259
handleDisplayOptionsIfNeeded(options: destinations.displayOptions)
252260

@@ -294,6 +302,7 @@ class GoogleMapsNavigationSessionManager: NSObject {
294302

295303
func clearDestinations() throws {
296304
try getNavigator().clearDestinations()
305+
_isNewNavigationSessionDetected = false
297306
}
298307

299308
func continueToNextDestination() throws -> NavigationWaypointDto? {
@@ -589,6 +598,15 @@ extension GoogleMapsNavigationSessionManager: GMSNavigatorListener {
589598
_ navigator: GMSNavigator,
590599
didUpdate navInfo: GMSNavigationNavInfo
591600
) {
601+
// Detect new navigation session start
602+
// This callback only fires when guidance is actively running, making it the ideal place
603+
// to detect session starts and match Android's behavior where NavigationSessionListener
604+
// fires when guidance begins
605+
if !_isNewNavigationSessionDetected {
606+
_isNewNavigationSessionDetected = true
607+
_navigationSessionEventApi?.onNewNavigationSession(completion: { _ in })
608+
}
609+
592610
if _sendTurnByTurnNavigationEvents {
593611
_navigationSessionEventApi?.onNavInfo(
594612
navInfo: Convert.convertNavInfo(

0 commit comments

Comments
 (0)