From 06a461f1adfc864bb1efae7d0ab665c96ef12167 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Wed, 19 Nov 2025 13:18:17 +0200 Subject: [PATCH 1/2] feat: add api to control guidance notifications --- .../GoogleMapsNavigationSessionManager.kt | 27 ++++ ...ogleMapsNavigationSessionMessageHandler.kt | 8 ++ .../maps/flutter/navigation/messages.g.kt | 56 ++++++++- .../t10_guidance_notifications_test.dart | 116 ++++++++++++++++++ example/lib/pages/navigation.dart | 18 +++ .../GoogleMapsNavigationSessionManager.swift | 10 ++ ...eMapsNavigationSessionMessageHandler.swift | 8 ++ .../messages.g.swift | 50 ++++++-- lib/src/method_channel/messages.g.dart | 64 +++++++++- lib/src/method_channel/session_api.dart | 45 +++++++ .../google_navigation_flutter_navigator.dart | 29 +++++ pigeons/messages.dart | 10 +- .../google_navigation_flutter_test.mocks.dart | 14 +++ test/messages_test.g.dart | 83 ++++++++++++- 14 files changed, 502 insertions(+), 36 deletions(-) create mode 100644 example/integration_test/t10_guidance_notifications_test.dart diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionManager.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionManager.kt index 7c76f4ce..6b8f25a3 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionManager.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionManager.kt @@ -74,6 +74,7 @@ constructor( private var navInfoObserver: Observer? = null private var weakLifecycleOwner: WeakReference? = null private var taskRemovedBehavior: @TaskRemovedBehavior Int = 0 + private var isGuidanceNotificationsEnabled: Boolean = true override fun onCreate(owner: LifecycleOwner) { weakLifecycleOwner = WeakReference(owner) @@ -495,6 +496,32 @@ constructor( getNavigator().setAudioGuidance(audioGuidanceSettings) } + /** + * Sets whether guidance notifications should be shown when the app is not in the foreground. On + * Android, this controls heads-up notifications for guidance events (turns, etc.). On iOS, this + * controls background notifications containing guidance information. + * + * This method must be called on the UI thread. Wraps [Navigator.setHeadsUpNotificationEnabled]. + * See + * [Google Navigation SDK for Android](https://developers.google.com/maps/documentation/navigation/android-sdk/reference/com/google/android/libraries/navigation/Navigator#setHeadsUpNotificationEnabled(boolean)). + */ + @Throws(FlutterError::class) + fun setGuidanceNotificationsEnabled(enabled: Boolean) { + isGuidanceNotificationsEnabled = enabled + val activity = getActivity() + activity.runOnUiThread { getNavigator().setHeadsUpNotificationEnabled(enabled) } + } + + /** + * Gets whether guidance notifications are enabled. On Android, returns the state of heads-up + * notifications. On iOS, returns the state of background notifications. + * + * @return true if guidance notifications are enabled, false otherwise. + */ + fun getGuidanceNotificationsEnabled(): Boolean { + return isGuidanceNotificationsEnabled + } + fun setSpeedAlertOptions(options: SpeedAlertOptions) { getNavigator().setSpeedAlertOptions(options) } diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionMessageHandler.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionMessageHandler.kt index 16db8d8b..8590d373 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionMessageHandler.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionMessageHandler.kt @@ -132,6 +132,14 @@ class GoogleMapsNavigationSessionMessageHandler( sessionManager.setAudioGuidance(audioGuidanceSettings) } + override fun setGuidanceNotificationsEnabled(enabled: Boolean) { + manager().setGuidanceNotificationsEnabled(enabled) + } + + override fun getGuidanceNotificationsEnabled(): Boolean { + return manager().getGuidanceNotificationsEnabled() + } + override fun setSpeedAlertOptions(options: SpeedAlertOptionsDto) { val newOptions = Convert.convertSpeedAlertOptionsFromDto(options) sessionManager.setSpeedAlertOptions(newOptions) diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/messages.g.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/messages.g.kt index 90cd57c8..b3825385 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/messages.g.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/messages.g.kt @@ -5693,7 +5693,6 @@ class ViewEventApi( /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface NavigationSessionApi { - /** General. */ fun createNavigationSession( abnormalTerminationReportingEnabled: Boolean, behavior: TaskRemovedBehaviorDto, @@ -5717,7 +5716,6 @@ interface NavigationSessionApi { fun getNavSDKVersion(): String - /** Navigation. */ fun isGuidanceRunning(): Boolean fun startGuidance() @@ -5742,7 +5740,10 @@ interface NavigationSessionApi { fun getCurrentRouteSegment(): RouteSegmentDto? - /** Simulation */ + fun setGuidanceNotificationsEnabled(enabled: Boolean) + + fun getGuidanceNotificationsEnabled(): Boolean + fun setUserLocation(location: LatLngDto) fun removeUserLocation() @@ -5773,15 +5774,13 @@ interface NavigationSessionApi { fun resumeSimulation() - /** Simulation (iOS only) */ + /** iOS-only method. */ fun allowBackgroundLocationUpdates(allow: Boolean) - /** Road snapped location updates. */ fun enableRoadSnappedLocationUpdates() fun disableRoadSnappedLocationUpdates() - /** Enable Turn-by-Turn navigation events. */ fun enableTurnByTurnNavigationEvents(numNextStepsToPreview: Long?) fun disableTurnByTurnNavigationEvents() @@ -6238,6 +6237,51 @@ interface NavigationSessionApi { channel.setMessageHandler(null) } } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.setGuidanceNotificationsEnabled$separatedMessageChannelSuffix", + codec, + ) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val enabledArg = args[0] as Boolean + val wrapped: List = + try { + api.setGuidanceNotificationsEnabled(enabledArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.getGuidanceNotificationsEnabled$separatedMessageChannelSuffix", + codec, + ) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = + try { + listOf(api.getGuidanceNotificationsEnabled()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel( diff --git a/example/integration_test/t10_guidance_notifications_test.dart b/example/integration_test/t10_guidance_notifications_test.dart new file mode 100644 index 00000000..765ff951 --- /dev/null +++ b/example/integration_test/t10_guidance_notifications_test.dart @@ -0,0 +1,116 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'shared.dart'; + +void main() { + patrol('Test guidance notifications enable and disable', ( + PatrolIntegrationTester $, + ) async { + // Initialize the navigator + await checkLocationDialogAcceptance($); + + /// Display navigation view. + final Key key = GlobalKey(); + await pumpNavigationView( + $, + GoogleMapsNavigationView( + key: key, + onViewCreated: (GoogleNavigationViewController controller) {}, + ), + ); + + // Initialize navigation session + await GoogleMapsNavigator.initializeNavigationSession(); + expect(await GoogleMapsNavigator.isInitialized(), true); + + // Test default state - should be enabled by default on most platforms + final bool initialState = + await GoogleMapsNavigator.getGuidanceNotificationsEnabled(); + $.log('Initial guidance notifications state: $initialState'); + + // Test enabling guidance notifications + await GoogleMapsNavigator.setGuidanceNotificationsEnabled(true); + bool currentState = + await GoogleMapsNavigator.getGuidanceNotificationsEnabled(); + expect( + currentState, + true, + reason: 'Guidance notifications should be enabled', + ); + $.log('Successfully enabled guidance notifications'); + + // Test disabling guidance notifications + await GoogleMapsNavigator.setGuidanceNotificationsEnabled(false); + currentState = await GoogleMapsNavigator.getGuidanceNotificationsEnabled(); + expect( + currentState, + false, + reason: 'Guidance notifications should be disabled', + ); + $.log('Successfully disabled guidance notifications'); + + // Test re-enabling guidance notifications + await GoogleMapsNavigator.setGuidanceNotificationsEnabled(true); + currentState = await GoogleMapsNavigator.getGuidanceNotificationsEnabled(); + expect( + currentState, + true, + reason: 'Guidance notifications should be enabled again', + ); + $.log('Successfully re-enabled guidance notifications'); + }); + + patrol('Test guidance notifications state persistence', ( + PatrolIntegrationTester $, + ) async { + // Initialize the navigator + await checkLocationDialogAcceptance($); + + /// Display navigation view. + final Key key = GlobalKey(); + await pumpNavigationView( + $, + GoogleMapsNavigationView( + key: key, + onViewCreated: (GoogleNavigationViewController controller) {}, + ), + ); + + // Initialize navigation session + await GoogleMapsNavigator.initializeNavigationSession(); + expect(await GoogleMapsNavigator.isInitialized(), true); + + // Set to a known state (disabled) + await GoogleMapsNavigator.setGuidanceNotificationsEnabled(false); + bool state = await GoogleMapsNavigator.getGuidanceNotificationsEnabled(); + expect(state, false, reason: 'Initial state should be disabled'); + + // Verify state is still disabled + state = await GoogleMapsNavigator.getGuidanceNotificationsEnabled(); + expect(state, false, reason: 'State should persist as disabled'); + $.log('State persisted correctly as disabled'); + + // Change to enabled + await GoogleMapsNavigator.setGuidanceNotificationsEnabled(true); + state = await GoogleMapsNavigator.getGuidanceNotificationsEnabled(); + expect(state, true, reason: 'State should be enabled'); + + // Verify state is still enabled + state = await GoogleMapsNavigator.getGuidanceNotificationsEnabled(); + expect(state, true, reason: 'State should persist as enabled'); + $.log('State persisted correctly as enabled'); + }); +} diff --git a/example/lib/pages/navigation.dart b/example/lib/pages/navigation.dart index 7dee8189..36037420 100644 --- a/example/lib/pages/navigation.dart +++ b/example/lib/pages/navigation.dart @@ -113,6 +113,7 @@ class _NavigationPageState extends ExamplePageState { bool _termsAndConditionsAccepted = false; bool _locationPermissionsAccepted = false; bool _turnByTurnNavigationEventEnabled = false; + bool _guidanceNotificationsEnabled = true; bool _isAutoScreenAvailable = false; @@ -232,6 +233,11 @@ class _NavigationPageState extends ExamplePageState { await _updateNavigatorInitializationState(); await _restorePossibleNavigatorState(); unawaited(_setDefaultUserLocationAfterDelay()); + + // Get the current guidance notifications state + _guidanceNotificationsEnabled = + await GoogleMapsNavigator.getGuidanceNotificationsEnabled(); + debugPrint('Navigator has been initialized: $_navigatorInitialized'); } setState(() {}); @@ -1580,6 +1586,18 @@ class _NavigationPageState extends ExamplePageState { }); }, ), + ExampleSwitch( + title: 'Guidance notifications', + initialValue: _guidanceNotificationsEnabled, + onChanged: (bool newValue) async { + await GoogleMapsNavigator.setGuidanceNotificationsEnabled( + newValue, + ); + setState(() { + _guidanceNotificationsEnabled = newValue; + }); + }, + ), ], ), const SizedBox(height: 10), diff --git a/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionManager.swift b/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionManager.swift index 9dbc7d97..d3babd6f 100644 --- a/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionManager.swift +++ b/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionManager.swift @@ -324,6 +324,16 @@ class GoogleMapsNavigationSessionManager: NSObject { } } + /// Sets whether guidance notifications should be sent when the app is in the background. + func setGuidanceNotificationsEnabled(enabled: Bool) throws { + try getNavigator().sendsBackgroundNotifications = enabled + } + + /// Gets whether guidance notifications are enabled. + func getGuidanceNotificationsEnabled() throws -> Bool { + try getNavigator().sendsBackgroundNotifications + } + /// Simulation func setUserLocation(location: LatLngDto) throws { try getSimulator().simulateLocation( diff --git a/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionMessageHandler.swift b/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionMessageHandler.swift index 8d3db8cf..14f3c571 100644 --- a/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionMessageHandler.swift +++ b/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionMessageHandler.swift @@ -123,6 +123,14 @@ class GoogleMapsNavigationSessionMessageHandler: NavigationSessionApi { try GoogleMapsNavigationSessionManager.shared.setAudioGuidance(settings: settings) } + func setGuidanceNotificationsEnabled(enabled: Bool) throws { + try GoogleMapsNavigationSessionManager.shared.setGuidanceNotificationsEnabled(enabled: enabled) + } + + func getGuidanceNotificationsEnabled() throws -> Bool { + try GoogleMapsNavigationSessionManager.shared.getGuidanceNotificationsEnabled() + } + /// Simulation func simulateLocationsAlongExistingRoute() throws { try GoogleMapsNavigationSessionManager.shared.simulateLocationsAlongExistingRoute() diff --git a/ios/google_navigation_flutter/Sources/google_navigation_flutter/messages.g.swift b/ios/google_navigation_flutter/Sources/google_navigation_flutter/messages.g.swift index d82bb8ad..7b3edc4e 100644 --- a/ios/google_navigation_flutter/Sources/google_navigation_flutter/messages.g.swift +++ b/ios/google_navigation_flutter/Sources/google_navigation_flutter/messages.g.swift @@ -4862,7 +4862,6 @@ class ViewEventApi: ViewEventApiProtocol { } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol NavigationSessionApi { - /// General. func createNavigationSession( abnormalTerminationReportingEnabled: Bool, behavior: TaskRemovedBehaviorDto, completion: @escaping (Result) -> Void) @@ -4874,7 +4873,6 @@ protocol NavigationSessionApi { func areTermsAccepted() throws -> Bool func resetTermsAccepted() throws func getNavSDKVersion() throws -> String - /// Navigation. func isGuidanceRunning() throws -> Bool func startGuidance() throws func stopGuidance() throws @@ -4888,7 +4886,8 @@ protocol NavigationSessionApi { func getRouteSegments() throws -> [RouteSegmentDto] func getTraveledRoute() throws -> [LatLngDto] func getCurrentRouteSegment() throws -> RouteSegmentDto? - /// Simulation + func setGuidanceNotificationsEnabled(enabled: Bool) throws + func getGuidanceNotificationsEnabled() throws -> Bool func setUserLocation(location: LatLngDto) throws func removeUserLocation() throws func simulateLocationsAlongExistingRoute() throws @@ -4905,12 +4904,10 @@ protocol NavigationSessionApi { completion: @escaping (Result) -> Void) func pauseSimulation() throws func resumeSimulation() throws - /// Simulation (iOS only) + /// iOS-only method. func allowBackgroundLocationUpdates(allow: Bool) throws - /// Road snapped location updates. func enableRoadSnappedLocationUpdates() throws func disableRoadSnappedLocationUpdates() throws - /// Enable Turn-by-Turn navigation events. func enableTurnByTurnNavigationEvents(numNextStepsToPreview: Int64?) throws func disableTurnByTurnNavigationEvents() throws func registerRemainingTimeOrDistanceChangedListener( @@ -4926,7 +4923,6 @@ class NavigationSessionApiSetup { messageChannelSuffix: String = "" ) { let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - /// General. let createNavigationSessionChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.createNavigationSession\(channelSuffix)", @@ -5058,7 +5054,6 @@ class NavigationSessionApiSetup { } else { getNavSDKVersionChannel.setMessageHandler(nil) } - /// Navigation. let isGuidanceRunningChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.isGuidanceRunning\(channelSuffix)", @@ -5259,7 +5254,40 @@ class NavigationSessionApiSetup { } else { getCurrentRouteSegmentChannel.setMessageHandler(nil) } - /// Simulation + let setGuidanceNotificationsEnabledChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.setGuidanceNotificationsEnabled\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setGuidanceNotificationsEnabledChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let enabledArg = args[0] as! Bool + do { + try api.setGuidanceNotificationsEnabled(enabled: enabledArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + setGuidanceNotificationsEnabledChannel.setMessageHandler(nil) + } + let getGuidanceNotificationsEnabledChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.getGuidanceNotificationsEnabled\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getGuidanceNotificationsEnabledChannel.setMessageHandler { _, reply in + do { + let result = try api.getGuidanceNotificationsEnabled() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getGuidanceNotificationsEnabledChannel.setMessageHandler(nil) + } let setUserLocationChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.setUserLocation\(channelSuffix)", @@ -5430,7 +5458,7 @@ class NavigationSessionApiSetup { } else { resumeSimulationChannel.setMessageHandler(nil) } - /// Simulation (iOS only) + /// iOS-only method. let allowBackgroundLocationUpdatesChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.allowBackgroundLocationUpdates\(channelSuffix)", @@ -5449,7 +5477,6 @@ class NavigationSessionApiSetup { } else { allowBackgroundLocationUpdatesChannel.setMessageHandler(nil) } - /// Road snapped location updates. let enableRoadSnappedLocationUpdatesChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.enableRoadSnappedLocationUpdates\(channelSuffix)", @@ -5482,7 +5509,6 @@ class NavigationSessionApiSetup { } else { disableRoadSnappedLocationUpdatesChannel.setMessageHandler(nil) } - /// Enable Turn-by-Turn navigation events. let enableTurnByTurnNavigationEventsChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.enableTurnByTurnNavigationEvents\(channelSuffix)", diff --git a/lib/src/method_channel/messages.g.dart b/lib/src/method_channel/messages.g.dart index 9ad8f4e9..b265ddea 100644 --- a/lib/src/method_channel/messages.g.dart +++ b/lib/src/method_channel/messages.g.dart @@ -6632,7 +6632,6 @@ class NavigationSessionApi { final String pigeonVar_messageChannelSuffix; - /// General. Future createNavigationSession( bool abnormalTerminationReportingEnabled, TaskRemovedBehaviorDto behavior, @@ -6841,7 +6840,6 @@ class NavigationSessionApi { } } - /// Navigation. Future isGuidanceRunning() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.isGuidanceRunning$pigeonVar_messageChannelSuffix'; @@ -7176,7 +7174,63 @@ class NavigationSessionApi { } } - /// Simulation + Future setGuidanceNotificationsEnabled(bool enabled) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.setGuidanceNotificationsEnabled$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [enabled], + ); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future getGuidanceNotificationsEnabled() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.getGuidanceNotificationsEnabled$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + Future setUserLocation(LatLngDto location) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.setUserLocation$pigeonVar_messageChannelSuffix'; @@ -7439,7 +7493,7 @@ class NavigationSessionApi { } } - /// Simulation (iOS only) + /// iOS-only method. Future allowBackgroundLocationUpdates(bool allow) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.allowBackgroundLocationUpdates$pigeonVar_messageChannelSuffix'; @@ -7467,7 +7521,6 @@ class NavigationSessionApi { } } - /// Road snapped location updates. Future enableRoadSnappedLocationUpdates() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.enableRoadSnappedLocationUpdates$pigeonVar_messageChannelSuffix'; @@ -7518,7 +7571,6 @@ class NavigationSessionApi { } } - /// Enable Turn-by-Turn navigation events. Future enableTurnByTurnNavigationEvents( int? numNextStepsToPreview, ) async { diff --git a/lib/src/method_channel/session_api.dart b/lib/src/method_channel/session_api.dart index bff7a621..dd2a6c58 100644 --- a/lib/src/method_channel/session_api.dart +++ b/lib/src/method_channel/session_api.dart @@ -296,6 +296,51 @@ class NavigationSessionAPIImpl { } } + /// Sets whether guidance notifications should be shown. + /// + /// Enables or disables guidance notifications when the app is not in the foreground. + /// + /// On Android, this controls heads-up notifications for guidance events (turns, etc.) + /// that are displayed when there is no map visible. + /// Maps to [Navigator.setHeadsUpNotificationEnabled](https://developers.google.com/maps/documentation/navigation/android-sdk/reference/com/google/android/libraries/navigation/Navigator#setHeadsUpNotificationEnabled(boolean)) + /// + /// On iOS, this controls background notifications containing guidance information + /// presented when the app is in the background. + /// Maps to [GMSNavigator.sendsBackgroundNotifications](https://developers.google.com/maps/documentation/navigation/ios-sdk/reference/objc/Classes/GMSNavigator#sendsbackgroundnotifications) + /// + /// Default: enabled on both platforms. + Future setGuidanceNotificationsEnabled(bool enabled) async { + try { + return await _sessionApi.setGuidanceNotificationsEnabled(enabled); + } on PlatformException catch (e) { + switch (e.code) { + case 'sessionNotInitialized': + throw const SessionNotInitializedException(); + default: + rethrow; + } + } + } + + /// Gets whether guidance notifications are enabled. + /// + /// On Android, returns the state of heads-up notifications. + /// + /// On iOS, returns the state of background notifications. + /// Maps to [GMSNavigator.sendsBackgroundNotifications](https://developers.google.com/maps/documentation/navigation/ios-sdk/reference/objc/Classes/GMSNavigator#sendsbackgroundnotifications) + Future getGuidanceNotificationsEnabled() async { + try { + return await _sessionApi.getGuidanceNotificationsEnabled(); + } on PlatformException catch (e) { + switch (e.code) { + case 'sessionNotInitialized': + throw const SessionNotInitializedException(); + default: + rethrow; + } + } + } + /// Sets user location. Future setUserLocation(LatLng location) async { try { diff --git a/lib/src/navigator/google_navigation_flutter_navigator.dart b/lib/src/navigator/google_navigation_flutter_navigator.dart index 59428c4b..bb5262ae 100644 --- a/lib/src/navigator/google_navigation_flutter_navigator.dart +++ b/lib/src/navigator/google_navigation_flutter_navigator.dart @@ -573,6 +573,35 @@ class GoogleMapsNavigator { .setAudioGuidance(settings); } + /// Sets whether guidance notifications should be shown. + /// + /// Enables or disables guidance notifications when the app is not in the foreground. + /// + /// On Android, this controls heads-up notifications for guidance events (turns, etc.) + /// that are displayed when there is no map visible. + /// Maps to [Navigator.setHeadsUpNotificationEnabled](https://developers.google.com/maps/documentation/navigation/android-sdk/reference/com/google/android/libraries/navigation/Navigator#setHeadsUpNotificationEnabled(boolean)) + /// + /// On iOS, this controls background notifications containing guidance information + /// presented when the app is in the background. + /// Maps to [GMSNavigator.sendsBackgroundNotifications](https://developers.google.com/maps/documentation/navigation/ios-sdk/reference/objc/Classes/GMSNavigator#sendsbackgroundnotifications) + /// + /// Default: enabled on both platforms. + static Future setGuidanceNotificationsEnabled(bool enabled) { + return GoogleMapsNavigationPlatform.instance.navigationSessionAPI + .setGuidanceNotificationsEnabled(enabled); + } + + /// Gets whether guidance notifications are enabled. + /// + /// On Android, returns the state of heads-up notifications. + /// + /// On iOS, returns the state of background notifications. + /// Maps to [GMSNavigator.sendsBackgroundNotifications](https://developers.google.com/maps/documentation/navigation/ios-sdk/reference/objc/Classes/GMSNavigator#sendsbackgroundnotifications) + static Future getGuidanceNotificationsEnabled() { + return GoogleMapsNavigationPlatform.instance.navigationSessionAPI + .getGuidanceNotificationsEnabled(); + } + /// Sets state of allow background location updates. (iOS only) /// /// Throws [UnsupportedError] on Android. diff --git a/pigeons/messages.dart b/pigeons/messages.dart index 9de0342b..31fa7739 100644 --- a/pigeons/messages.dart +++ b/pigeons/messages.dart @@ -1217,7 +1217,6 @@ enum TaskRemovedBehaviorDto { @HostApi(dartHostTestHandler: 'TestNavigationSessionApi') abstract class NavigationSessionApi { - /// General. @async void createNavigationSession( bool abnormalTerminationReportingEnabled, @@ -1235,7 +1234,6 @@ abstract class NavigationSessionApi { void resetTermsAccepted(); String getNavSDKVersion(); - /// Navigation. bool isGuidanceRunning(); void startGuidance(); void stopGuidance(); @@ -1250,7 +1248,9 @@ abstract class NavigationSessionApi { List getTraveledRoute(); RouteSegmentDto? getCurrentRouteSegment(); - /// Simulation + void setGuidanceNotificationsEnabled(bool enabled); + bool getGuidanceNotificationsEnabled(); + void setUserLocation(LatLngDto location); void removeUserLocation(); void simulateLocationsAlongExistingRoute(); @@ -1275,14 +1275,12 @@ abstract class NavigationSessionApi { void pauseSimulation(); void resumeSimulation(); - /// Simulation (iOS only) + /// iOS-only method. void allowBackgroundLocationUpdates(bool allow); - /// Road snapped location updates. void enableRoadSnappedLocationUpdates(); void disableRoadSnappedLocationUpdates(); - /// Enable Turn-by-Turn navigation events. void enableTurnByTurnNavigationEvents(int? numNextStepsToPreview); void disableTurnByTurnNavigationEvents(); diff --git a/test/google_navigation_flutter_test.mocks.dart b/test/google_navigation_flutter_test.mocks.dart index c5916b3e..03a06242 100644 --- a/test/google_navigation_flutter_test.mocks.dart +++ b/test/google_navigation_flutter_test.mocks.dart @@ -230,6 +230,20 @@ class MockTestNavigationSessionApi extends _i1.Mock ) as List<_i2.LatLngDto>); + @override + void setGuidanceNotificationsEnabled(bool? enabled) => super.noSuchMethod( + Invocation.method(#setGuidanceNotificationsEnabled, [enabled]), + returnValueForMissingStub: null, + ); + + @override + bool getGuidanceNotificationsEnabled() => + (super.noSuchMethod( + Invocation.method(#getGuidanceNotificationsEnabled, []), + returnValue: false, + ) + as bool); + @override void setUserLocation(_i2.LatLngDto? location) => super.noSuchMethod( Invocation.method(#setUserLocation, [location]), diff --git a/test/messages_test.g.dart b/test/messages_test.g.dart index f94c7885..5b322b5e 100644 --- a/test/messages_test.g.dart +++ b/test/messages_test.g.dart @@ -4967,7 +4967,6 @@ abstract class TestNavigationSessionApi { TestDefaultBinaryMessengerBinding.instance; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - /// General. Future createNavigationSession( bool abnormalTerminationReportingEnabled, TaskRemovedBehaviorDto behavior, @@ -4989,7 +4988,6 @@ abstract class TestNavigationSessionApi { String getNavSDKVersion(); - /// Navigation. bool isGuidanceRunning(); void startGuidance(); @@ -5014,7 +5012,10 @@ abstract class TestNavigationSessionApi { RouteSegmentDto? getCurrentRouteSegment(); - /// Simulation + void setGuidanceNotificationsEnabled(bool enabled); + + bool getGuidanceNotificationsEnabled(); + void setUserLocation(LatLngDto location); void removeUserLocation(); @@ -5045,15 +5046,13 @@ abstract class TestNavigationSessionApi { void resumeSimulation(); - /// Simulation (iOS only) + /// iOS-only method. void allowBackgroundLocationUpdates(bool allow); - /// Road snapped location updates. void enableRoadSnappedLocationUpdates(); void disableRoadSnappedLocationUpdates(); - /// Enable Turn-by-Turn navigation events. void enableTurnByTurnNavigationEvents(int? numNextStepsToPreview); void disableTurnByTurnNavigationEvents(); @@ -5743,6 +5742,78 @@ abstract class TestNavigationSessionApi { }); } } + { + final BasicMessageChannel + pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.setGuidanceNotificationsEnabled$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, ( + Object? message, + ) async { + assert( + message != null, + 'Argument for dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.setGuidanceNotificationsEnabled was null.', + ); + final List args = (message as List?)!; + final bool? arg_enabled = (args[0] as bool?); + assert( + arg_enabled != null, + 'Argument for dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.setGuidanceNotificationsEnabled was null, expected non-null bool.', + ); + try { + api.setGuidanceNotificationsEnabled(arg_enabled!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException( + code: 'error', + message: e.toString(), + ), + ); + } + }); + } + } + { + final BasicMessageChannel + pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.getGuidanceNotificationsEnabled$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, ( + Object? message, + ) async { + try { + final bool output = api.getGuidanceNotificationsEnabled(); + return [output]; + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException( + code: 'error', + message: e.toString(), + ), + ); + } + }); + } + } { final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( From 892e6e638453110b0ca161198ddb0b1eae2bca7c Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Wed, 19 Nov 2025 13:28:43 +0200 Subject: [PATCH 2/2] chore: move guidance notification handling to the navigator holder on Android --- .../GoogleMapsNavigationSessionManager.kt | 6 ++--- .../navigation/GoogleMapsNavigatorHolder.kt | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionManager.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionManager.kt index 6b8f25a3..811ee763 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionManager.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionManager.kt @@ -74,7 +74,6 @@ constructor( private var navInfoObserver: Observer? = null private var weakLifecycleOwner: WeakReference? = null private var taskRemovedBehavior: @TaskRemovedBehavior Int = 0 - private var isGuidanceNotificationsEnabled: Boolean = true override fun onCreate(owner: LifecycleOwner) { weakLifecycleOwner = WeakReference(owner) @@ -507,9 +506,8 @@ constructor( */ @Throws(FlutterError::class) fun setGuidanceNotificationsEnabled(enabled: Boolean) { - isGuidanceNotificationsEnabled = enabled val activity = getActivity() - activity.runOnUiThread { getNavigator().setHeadsUpNotificationEnabled(enabled) } + activity.runOnUiThread { GoogleMapsNavigatorHolder.setGuidanceNotificationsEnabled(enabled) } } /** @@ -519,7 +517,7 @@ constructor( * @return true if guidance notifications are enabled, false otherwise. */ fun getGuidanceNotificationsEnabled(): Boolean { - return isGuidanceNotificationsEnabled + return GoogleMapsNavigatorHolder.getGuidanceNotificationsEnabled() } fun setSpeedAlertOptions(options: SpeedAlertOptions) { diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigatorHolder.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigatorHolder.kt index 37371160..caf710c1 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigatorHolder.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigatorHolder.kt @@ -44,6 +44,9 @@ object GoogleMapsNavigatorHolder { private var turnByTurnServiceRegistered = false private val navInfoObservers = mutableListOf>() + // Guidance notifications management + private var isGuidanceNotificationsEnabled = true + @Synchronized fun getNavigator(): Navigator? = navigator @Synchronized @@ -138,6 +141,23 @@ object GoogleMapsNavigatorHolder { return true } + /** Sets whether guidance notifications should be shown when the app is not in the foreground. */ + @Synchronized + fun setGuidanceNotificationsEnabled(enabled: Boolean) { + isGuidanceNotificationsEnabled = enabled + navigator?.setHeadsUpNotificationEnabled(enabled) + } + + /** + * Gets whether guidance notifications are enabled. Returns the state of heads-up notifications. + * + * @return true if guidance notifications are enabled, false otherwise. + */ + @Synchronized + fun getGuidanceNotificationsEnabled(): Boolean { + return isGuidanceNotificationsEnabled + } + @Synchronized fun reset() { // Clean up turn-by-turn service @@ -150,6 +170,11 @@ object GoogleMapsNavigatorHolder { turnByTurnServiceRegistered = false } + if (isGuidanceNotificationsEnabled == false) { + navigator?.setHeadsUpNotificationEnabled(true) + isGuidanceNotificationsEnabled = true + } + navigator = null initializationState = GoogleNavigatorInitializationState.NOT_INITIALIZED initializationCallbacks.clear()