Skip to content

Commit fb0b623

Browse files
committed
feat: add interface for new navigation session detection
1 parent cb7d61e commit fb0b623

File tree

12 files changed

+272
-58
lines changed

12 files changed

+272
-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: 64 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,7 +76,26 @@ 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
GoogleMapsNavigator.setOnArrivalListener(onArrivalEvent);
96+
GoogleMapsNavigator.setOnNewNavigationSessionListener(
97+
onNewNavigationSession,
98+
);
9099

91100
/// Simulate location and test it.
92101
await setSimulatedUserLocationWithCheck(
@@ -140,6 +149,18 @@ void main() {
140149
await GoogleMapsNavigator.simulator.simulateLocationsAlongExistingRoute();
141150

142151
expect(await GoogleMapsNavigator.isGuidanceRunning(), true);
152+
153+
/// Wait for new navigation session event.
154+
await newSessionFired.future.timeout(
155+
const Duration(seconds: 30),
156+
onTimeout:
157+
() =>
158+
throw TimeoutException(
159+
'New navigation session event was not fired',
160+
),
161+
);
162+
expect(newSessionFired.isCompleted, true);
163+
143164
await hasArrived.future;
144165
expect(await GoogleMapsNavigator.isGuidanceRunning(), false);
145166

@@ -150,24 +171,14 @@ void main() {
150171
'Test navigating to multiple destinations',
151172
(PatrolIntegrationTester $) async {
152173
final Completer<void> navigationFinished = Completer<void>();
174+
final Completer<void> newSessionFired = Completer<void>();
153175
int arrivalEventCount = 0;
154176
List<NavigationWaypoint> waypoints = <NavigationWaypoint>[];
155177

156178
/// Set up navigation view and controller.
157179
final GoogleNavigationViewController viewController =
158180
await startNavigationWithoutDestination($);
159181

160-
/// Set audio guidance settings.
161-
/// Cannot be verified, because native SDK lacks getter methods,
162-
/// but exercise the API for basic sanity testing
163-
final NavigationAudioGuidanceSettings settings =
164-
NavigationAudioGuidanceSettings(
165-
isBluetoothAudioEnabled: false,
166-
isVibrationEnabled: false,
167-
guidanceType: NavigationAudioGuidanceType.alertsOnly,
168-
);
169-
await GoogleMapsNavigator.setAudioGuidance(settings);
170-
171182
/// Specify tolerance and navigation destination coordinates.
172183
const double tolerance = 0.001;
173184
const double midLat = 68.59781164189049,
@@ -228,7 +239,26 @@ void main() {
228239
}
229240
}
230241

242+
/// Set up listener for new navigation session event.
243+
Future<void> onNewNavigationSession() async {
244+
newSessionFired.complete();
245+
246+
/// Sets audio guidance settings for the current navigation session.
247+
/// Cannot be verified, because native SDK lacks getter methods,
248+
/// but exercise the API for basic sanity testing.
249+
await GoogleMapsNavigator.setAudioGuidance(
250+
NavigationAudioGuidanceSettings(
251+
isBluetoothAudioEnabled: true,
252+
isVibrationEnabled: true,
253+
guidanceType: NavigationAudioGuidanceType.alertsAndGuidance,
254+
),
255+
);
256+
}
257+
231258
GoogleMapsNavigator.setOnArrivalListener(onArrivalEvent);
259+
GoogleMapsNavigator.setOnNewNavigationSessionListener(
260+
onNewNavigationSession,
261+
);
232262

233263
/// Simulate location and test it.
234264
await setSimulatedUserLocationWithCheck(
@@ -308,6 +338,18 @@ void main() {
308338
);
309339

310340
expect(await GoogleMapsNavigator.isGuidanceRunning(), true);
341+
342+
/// Wait for new navigation session event.
343+
await newSessionFired.future.timeout(
344+
const Duration(seconds: 30),
345+
onTimeout:
346+
() =>
347+
throw TimeoutException(
348+
'New navigation session event was not fired',
349+
),
350+
);
351+
expect(newSessionFired.isCompleted, true);
352+
311353
await navigationFinished.future;
312354
expect(await GoogleMapsNavigator.isGuidanceRunning(), false);
313355

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)