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 3cbe415f..d82febaa 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 @@ -120,10 +120,12 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven RoadSnappedLocationProvider.GpsAvailabilityEnhancedLocationListener? = null private var speedingListener: SpeedingListener? = null + private var navigationSessionListener: Navigator.NavigationSessionListener? = null private var weakActivity: WeakReference? = null private var turnByTurnEventsEnabled: Boolean = false private var weakLifecycleOwner: WeakReference? = null private var taskRemovedBehavior: @TaskRemovedBehavior Int = 0 + private var isGuidanceNotificationsEnabled: Boolean = true override fun onCreate(owner: LifecycleOwner) { weakLifecycleOwner = WeakReference(owner) @@ -331,6 +333,10 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven navigator.setSpeedingListener(null) speedingListener = null } + if (navigationSessionListener != null) { + navigator.removeNavigationSessionListener(navigationSessionListener) + navigationSessionListener = null + } } if (roadSnappedLocationListener != null) { disableRoadSnappedLocationUpdates() @@ -407,6 +413,16 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven } navigator.setSpeedingListener(speedingListener) } + + if (navigationSessionListener == null) { + navigationSessionListener = + object : Navigator.NavigationSessionListener { + override fun onNewNavigationSession() { + navigationSessionEventApi.onNewNavigationSession {} + } + } + navigator.addNavigationSessionListener(navigationSessionListener) + } } /** @@ -511,6 +527,32 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven 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 b443bfee..a8783005 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 @@ -133,6 +133,14 @@ class GoogleMapsNavigationSessionMessageHandler : NavigationSessionApi { manager().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) manager().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 7c938d0f..3e2e3f86 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() @@ -6236,6 +6235,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( @@ -6808,6 +6852,26 @@ class NavigationSessionEventApi( } } } + + /** Navigation session event. Called when a new navigation session starts with active guidance. */ + fun onNewNavigationSession(callback: (Result) -> Unit) { + val separatedMessageChannelSuffix = + if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = + "dev.flutter.pigeon.google_navigation_flutter.NavigationSessionEventApi.onNewNavigationSession$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(MessagesPigeonUtils.createConnectionError(channelName))) + } + } + } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ diff --git a/example/integration_test/t03_navigation_test.dart b/example/integration_test/t03_navigation_test.dart index e7569156..3c7010f5 100644 --- a/example/integration_test/t03_navigation_test.dart +++ b/example/integration_test/t03_navigation_test.dart @@ -59,22 +59,12 @@ void main() { PatrolIntegrationTester $, ) async { final Completer hasArrived = Completer(); + final Completer newSessionFired = Completer(); /// Set up navigation view and controller. final GoogleNavigationViewController viewController = await startNavigationWithoutDestination($); - /// Set audio guidance settings. - /// Cannot be verified, because native SDK lacks getter methods, - /// but exercise the API for basic sanity testing - final NavigationAudioGuidanceSettings settings = - NavigationAudioGuidanceSettings( - isBluetoothAudioEnabled: true, - isVibrationEnabled: true, - guidanceType: NavigationAudioGuidanceType.alertsAndGuidance, - ); - await GoogleMapsNavigator.setAudioGuidance(settings); - /// Specify tolerance and navigation end coordinates. const double tolerance = 0.001; const double endLat = 68.59451829688189, endLng = 23.512277951523007; @@ -86,7 +76,26 @@ void main() { await GoogleMapsNavigator.stopGuidance(); } + /// Set up listener for new navigation session event. + Future onNewNavigationSession() async { + newSessionFired.complete(); + + /// Sets audio guidance settings for the current navigation session. + /// Cannot be verified, because native SDK lacks getter methods, + /// but exercise the API for basic sanity testing. + await GoogleMapsNavigator.setAudioGuidance( + NavigationAudioGuidanceSettings( + isBluetoothAudioEnabled: true, + isVibrationEnabled: true, + guidanceType: NavigationAudioGuidanceType.alertsAndGuidance, + ), + ); + } + GoogleMapsNavigator.setOnArrivalListener(onArrivalEvent); + GoogleMapsNavigator.setOnNewNavigationSessionListener( + onNewNavigationSession, + ); /// Simulate location and test it. await setSimulatedUserLocationWithCheck( @@ -140,6 +149,18 @@ void main() { await GoogleMapsNavigator.simulator.simulateLocationsAlongExistingRoute(); expect(await GoogleMapsNavigator.isGuidanceRunning(), true); + + /// Wait for new navigation session event. + await newSessionFired.future.timeout( + const Duration(seconds: 30), + onTimeout: + () => + throw TimeoutException( + 'New navigation session event was not fired', + ), + ); + expect(newSessionFired.isCompleted, true); + await hasArrived.future; expect(await GoogleMapsNavigator.isGuidanceRunning(), false); @@ -150,6 +171,7 @@ void main() { 'Test navigating to multiple destinations', (PatrolIntegrationTester $) async { final Completer navigationFinished = Completer(); + final Completer newSessionFired = Completer(); int arrivalEventCount = 0; List waypoints = []; @@ -157,17 +179,6 @@ void main() { final GoogleNavigationViewController viewController = await startNavigationWithoutDestination($); - /// Set audio guidance settings. - /// Cannot be verified, because native SDK lacks getter methods, - /// but exercise the API for basic sanity testing - final NavigationAudioGuidanceSettings settings = - NavigationAudioGuidanceSettings( - isBluetoothAudioEnabled: false, - isVibrationEnabled: false, - guidanceType: NavigationAudioGuidanceType.alertsOnly, - ); - await GoogleMapsNavigator.setAudioGuidance(settings); - /// Specify tolerance and navigation destination coordinates. const double tolerance = 0.001; const double midLat = 68.59781164189049, @@ -228,7 +239,26 @@ void main() { } } + /// Set up listener for new navigation session event. + Future onNewNavigationSession() async { + newSessionFired.complete(); + + /// Sets audio guidance settings for the current navigation session. + /// Cannot be verified, because native SDK lacks getter methods, + /// but exercise the API for basic sanity testing. + await GoogleMapsNavigator.setAudioGuidance( + NavigationAudioGuidanceSettings( + isBluetoothAudioEnabled: true, + isVibrationEnabled: true, + guidanceType: NavigationAudioGuidanceType.alertsAndGuidance, + ), + ); + } + GoogleMapsNavigator.setOnArrivalListener(onArrivalEvent); + GoogleMapsNavigator.setOnNewNavigationSessionListener( + onNewNavigationSession, + ); /// Simulate location and test it. await setSimulatedUserLocationWithCheck( @@ -308,6 +338,18 @@ void main() { ); expect(await GoogleMapsNavigator.isGuidanceRunning(), true); + + /// Wait for new navigation session event. + await newSessionFired.future.timeout( + const Duration(seconds: 30), + onTimeout: + () => + throw TimeoutException( + 'New navigation session event was not fired', + ), + ); + expect(newSessionFired.isCompleted, true); + await navigationFinished.future; expect(await GoogleMapsNavigator.isGuidanceRunning(), false); diff --git a/example/integration_test/t09_guidance_notifications_test.dart b/example/integration_test/t09_guidance_notifications_test.dart new file mode 100644 index 00000000..765ff951 --- /dev/null +++ b/example/integration_test/t09_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/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index b3aaa733..1f6b98f1 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 12.0 + 13.0 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 14aea2a1..f8750f15 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ D3A3B5ED895816BDF2A70351 /* Pods_RunnerCarPlay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B269F01B5CD151328AA7F8EB /* Pods_RunnerCarPlay.framework */; }; DA52652595AF0077243F8014 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64B801515FD75D36D58A86FE /* Pods_Runner.framework */; }; EDFE577D2F64CF5D3712A4E9 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64B801515FD75D36D58A86FE /* Pods_Runner.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -115,6 +116,7 @@ D775A5369CBCF55F2213A29D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; F66F7193CD255958326CC224 /* Pods-RunnerCarPlay.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerCarPlay.release.xcconfig"; path = "Target Support Files/Pods-RunnerCarPlay/Pods-RunnerCarPlay.release.xcconfig"; sourceTree = ""; }; FD0B54A81651AC7754FA8D08 /* Pods-Runner-RunnerUITests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-RunnerUITests.profile.xcconfig"; path = "Target Support Files/Pods-Runner-RunnerUITests/Pods-Runner-RunnerUITests.profile.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -139,6 +141,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, EDFE577D2F64CF5D3712A4E9 /* Pods_Runner.framework in Frameworks */, DA52652595AF0077243F8014 /* Pods_Runner.framework in Frameworks */, ); @@ -174,6 +177,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -327,6 +331,9 @@ productType = "com.apple.product-type.bundle.ui-testing"; }; 97C146ED1CF9000F007C117D /* Runner */ = { + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -353,6 +360,9 @@ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; @@ -1294,6 +1304,18 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..6a17a6f5 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "ios-maps-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googlemaps/ios-maps-sdk", + "state" : { + "revision" : "9c540f3b475a800e947a09b8903b212a6634cf30", + "version" : "10.0.0" + } + }, + { + "identity" : "ios-navigation-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googlemaps/ios-navigation-sdk", + "state" : { + "revision" : "a3faa12da9a957420da8e1b448022f365fbc8400", + "version" : "10.0.0" + } + } + ], + "version" : 2 +} diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3ae71d86..49b7aacd 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + { int _onRecenterButtonClickedEventCallCount = 0; int _onRemainingTimeOrDistanceChangedEventCallCount = 0; int _onNavigationUIEnabledChangedEventCallCount = 0; + int _onNewNavigationSessionEventCallCount = 0; bool _navigationHeaderEnabled = true; bool _navigationFooterEnabled = true; @@ -113,6 +114,7 @@ class _NavigationPageState extends ExamplePageState { bool _termsAndConditionsAccepted = false; bool _locationPermissionsAccepted = false; bool _turnByTurnNavigationEventEnabled = false; + bool _guidanceNotificationsEnabled = true; bool _isAutoScreenAvailable = false; @@ -147,6 +149,7 @@ class _NavigationPageState extends ExamplePageState { _roadSnappedLocationUpdatedSubscription; StreamSubscription? _roadSnappedRawLocationUpdatedSubscription; + StreamSubscription? _newNavigationSessionSubscription; int _nextWaypointIndex = 0; @@ -232,6 +235,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(() {}); @@ -379,6 +387,11 @@ class _NavigationPageState extends ExamplePageState { await GoogleMapsNavigator.setRoadSnappedRawLocationUpdatedListener( _onRoadSnappedRawLocationUpdatedEvent, ); + + _newNavigationSessionSubscription = + GoogleMapsNavigator.setOnNewNavigationSessionListener( + _onNewNavigationSessionEvent, + ); } void _clearListeners() { @@ -408,6 +421,24 @@ class _NavigationPageState extends ExamplePageState { _roadSnappedRawLocationUpdatedSubscription?.cancel(); _roadSnappedRawLocationUpdatedSubscription = null; + + _newNavigationSessionSubscription?.cancel(); + _newNavigationSessionSubscription = null; + } + + void _onNewNavigationSessionEvent() { + if (!mounted) { + return; + } + + setState(() { + _onNewNavigationSessionEventCallCount += 1; + }); + + showMessage('New navigation session started'); + + // Set audio guidance settings for the new navigation session. + unawaited(_setAudioGuidance()); } void _onRoadSnappedLocationUpdatedEvent( @@ -517,6 +548,16 @@ class _NavigationPageState extends ExamplePageState { await _getInitialViewStates(); } + Future _setAudioGuidance() async { + await GoogleMapsNavigator.setAudioGuidance( + NavigationAudioGuidanceSettings( + isBluetoothAudioEnabled: true, + isVibrationEnabled: true, + guidanceType: NavigationAudioGuidanceType.alertsAndGuidance, + ), + ); + } + Future _getInitialViewStates() async { assert(_navigationViewController != null); if (_navigationViewController != null) { @@ -1445,6 +1486,14 @@ class _NavigationPageState extends ExamplePageState { ), ), ), + Card( + child: ListTile( + title: const Text('New navigation session event call count'), + trailing: Text( + _onNewNavigationSessionEventCallCount.toString(), + ), + ), + ), ], ), ); @@ -1580,6 +1629,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 2066f325..353231d7 100644 --- a/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionManager.swift +++ b/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionManager.swift @@ -64,6 +64,8 @@ class GoogleMapsNavigationSessionManager: NSObject { private var _numTurnByTurnNextStepsToPreview = Int64.max + private var _isNewNavigationSessionDetected = false + func getNavigator() throws -> GMSNavigator { guard let _session else { throw GoogleMapsNavigationSessionManagerError.sessionNotInitialized } guard let navigator = _session.navigator @@ -217,6 +219,7 @@ class GoogleMapsNavigationSessionManager: NSObject { func stopGuidance() throws { try getNavigator().isGuidanceActive = false + _isNewNavigationSessionDetected = false } func isGuidanceRunning() throws -> Bool { @@ -243,6 +246,11 @@ class GoogleMapsNavigationSessionManager: NSObject { completion: @escaping (Result) -> Void ) { do { + // Reset session detection state to allow onNewNavigationSession to fire again + // This mimics Android's behavior where the event fires each time setDestinations + // is called while guidance is running + _isNewNavigationSessionDetected = false + // If the session has view attached, enable given display options. handleDisplayOptionsIfNeeded(options: destinations.displayOptions) @@ -290,6 +298,7 @@ class GoogleMapsNavigationSessionManager: NSObject { func clearDestinations() throws { try getNavigator().clearDestinations() + _isNewNavigationSessionDetected = false } func continueToNextDestination() throws -> NavigationWaypointDto? { @@ -320,6 +329,22 @@ class GoogleMapsNavigationSessionManager: NSObject { } } + /// Sets whether guidance notifications should be sent when the app is in the background. + /// On iOS, this controls background notifications containing guidance information. + /// On Android, this controls heads-up notifications for guidance events. + /// + /// Wraps GMSNavigator.sendsBackgroundNotifications on iOS. + func setGuidanceNotificationsEnabled(enabled: Bool) throws { + try getNavigator().sendsBackgroundNotifications = enabled + } + + /// Gets whether guidance notifications are enabled. + /// On iOS, returns the state of background notifications. + /// On Android, returns the state of heads-up notifications. + func getGuidanceNotificationsEnabled() throws -> Bool { + try getNavigator().sendsBackgroundNotifications + } + /// Simulation func setUserLocation(location: LatLngDto) throws { try getSimulator().simulateLocation( @@ -585,6 +610,15 @@ extension GoogleMapsNavigationSessionManager: GMSNavigatorListener { _ navigator: GMSNavigator, didUpdate navInfo: GMSNavigationNavInfo ) { + // Detect new navigation session start + // This callback only fires when guidance is actively running, making it the ideal place + // to detect session starts and match Android's behavior where NavigationSessionListener + // fires when guidance begins + if !_isNewNavigationSessionDetected { + _isNewNavigationSessionDetected = true + _navigationSessionEventApi?.onNewNavigationSession(completion: { _ in }) + } + if _sendTurnByTurnNavigationEvents { _navigationSessionEventApi?.onNavInfo( navInfo: Convert.convertNavInfo( 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 4a73622f..c081a520 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 150f6fbc..3dea29d6 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)", @@ -5056,7 +5052,6 @@ class NavigationSessionApiSetup { } else { getNavSDKVersionChannel.setMessageHandler(nil) } - /// Navigation. let isGuidanceRunningChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.isGuidanceRunning\(channelSuffix)", @@ -5257,7 +5252,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)", @@ -5428,7 +5456,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)", @@ -5447,7 +5475,6 @@ class NavigationSessionApiSetup { } else { allowBackgroundLocationUpdatesChannel.setMessageHandler(nil) } - /// Road snapped location updates. let enableRoadSnappedLocationUpdatesChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.enableRoadSnappedLocationUpdates\(channelSuffix)", @@ -5480,7 +5507,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)", @@ -5563,6 +5589,9 @@ protocol NavigationSessionEventApiProtocol { /// Turn-by-Turn navigation events. func onNavInfo( navInfo navInfoArg: NavInfoDto, completion: @escaping (Result) -> Void) + /// Navigation session event. Called when a new navigation + /// session starts with active guidance. + func onNewNavigationSession(completion: @escaping (Result) -> Void) } class NavigationSessionEventApi: NavigationSessionEventApiProtocol { private let binaryMessenger: FlutterBinaryMessenger @@ -5794,6 +5823,28 @@ class NavigationSessionEventApi: NavigationSessionEventApiProtocol { } } } + /// Navigation session event. Called when a new navigation + /// session starts with active guidance. + func onNewNavigationSession(completion: @escaping (Result) -> Void) { + let channelName: String = + "dev.flutter.pigeon.google_navigation_flutter.NavigationSessionEventApi.onNewNavigationSession\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel( + name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol AutoMapViewApi { diff --git a/lib/src/google_navigation_flutter.dart b/lib/src/google_navigation_flutter.dart index df1dc9a5..dc92d3cf 100644 --- a/lib/src/google_navigation_flutter.dart +++ b/lib/src/google_navigation_flutter.dart @@ -68,6 +68,9 @@ typedef OnRemainingTimeOrDistanceChangedEventCallback = /// Called on navigation info event. typedef OnNavInfoEventCallback = void Function(NavInfoEvent onNavInfo); +/// Called on new navigation session event. +typedef OnNewNavigationSessionCallback = void Function(); + /// Called during marker click event. typedef OnMarkerClicked = void Function(String markerId); diff --git a/lib/src/method_channel/messages.g.dart b/lib/src/method_channel/messages.g.dart index 614725e8..6ac244a2 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, @@ -6839,7 +6838,6 @@ class NavigationSessionApi { } } - /// Navigation. Future isGuidanceRunning() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.isGuidanceRunning$pigeonVar_messageChannelSuffix'; @@ -7174,7 +7172,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'; @@ -7437,7 +7491,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'; @@ -7465,7 +7519,6 @@ class NavigationSessionApi { } } - /// Road snapped location updates. Future enableRoadSnappedLocationUpdates() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.enableRoadSnappedLocationUpdates$pigeonVar_messageChannelSuffix'; @@ -7516,7 +7569,6 @@ class NavigationSessionApi { } } - /// Enable Turn-by-Turn navigation events. Future enableTurnByTurnNavigationEvents( int? numNextStepsToPreview, ) async { @@ -7635,6 +7687,10 @@ abstract class NavigationSessionEventApi { /// Turn-by-Turn navigation events. void onNavInfo(NavInfoDto navInfo); + /// Navigation session event. Called when a new navigation + /// session starts with active guidance. + void onNewNavigationSession(); + static void setUp( NavigationSessionEventApi? api, { BinaryMessenger? binaryMessenger, @@ -7962,6 +8018,30 @@ abstract class NavigationSessionEventApi { }); } } + { + final BasicMessageChannel + pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.google_navigation_flutter.NavigationSessionEventApi.onNewNavigationSession$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + api.onNewNavigationSession(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); + } + }); + } + } } } diff --git a/lib/src/method_channel/session_api.dart b/lib/src/method_channel/session_api.dart index 4cde5fed..06f457f0 100644 --- a/lib/src/method_channel/session_api.dart +++ b/lib/src/method_channel/session_api.dart @@ -288,6 +288,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 { @@ -660,6 +705,13 @@ class NavigationSessionAPIImpl { Stream getNavInfoStream() { return _sessionEventStreamController.stream.whereType(); } + + /// Get new navigation session event stream from the navigation session. + Stream getNewNavigationSessionEventStream() { + return _sessionEventStreamController.stream + .whereType<_NewNavigationSessionEvent>() + .map((_NewNavigationSessionEvent event) => ()); + } } /// Implementation for navigation session event API event handling. @@ -740,6 +792,11 @@ class NavigationSessionEventApiImpl implements NavigationSessionEventApi { NavInfoEvent(navInfo: navInfo.toNavInfo()), ); } + + @override + void onNewNavigationSession() { + sessionEventStreamController.add(_NewNavigationSessionEvent()); + } } /// Event wrapper for a route update events. @@ -750,3 +807,6 @@ class _ReroutingEvent {} /// Event wrapper for a traffic updated events. class _TrafficUpdatedEvent {} + +/// Event wrapper for a new navigation session event. +class _NewNavigationSessionEvent {} diff --git a/lib/src/navigator/google_navigation_flutter_navigator.dart b/lib/src/navigator/google_navigation_flutter_navigator.dart index 2bda2f25..f13621bb 100644 --- a/lib/src/navigator/google_navigation_flutter_navigator.dart +++ b/lib/src/navigator/google_navigation_flutter_navigator.dart @@ -126,6 +126,41 @@ class GoogleMapsNavigator { .listen(listener); } + /// Sets the event channel listener for new navigation session events. + /// + /// This event is triggered when a new navigation session starts with guidance active. + /// On both Android and iOS, this fires when: + /// - A route has been set via [setDestinations] + /// - Guidance is started with [startGuidance] + /// + /// On Android, this wraps Navigator.NavigationSessionListener.onNewNavigationSession. + /// On iOS, this is detected internally when a route is set and guidance becomes active. + /// + /// Returns a [StreamSubscription] for new navigation session events. + /// This subscription must be canceled using `cancel()` when it is no longer + /// needed to stop receiving events and allow the stream to perform necessary + /// cleanup, such as releasing resources or shutting down event sources. The + /// cleanup is asynchronous, and the `cancel()` method returns a Future that + /// completes once the cleanup is done. + /// + /// Example usage: + /// ```dart + /// final subscription = setOnNewNavigationSessionListener(() { + /// print('New navigation session started'); + /// }); + /// // When done with the subscription + /// await subscription.cancel(); + /// ``` + static StreamSubscription setOnNewNavigationSessionListener( + OnNewNavigationSessionCallback listener, + ) { + return GoogleMapsNavigationPlatform.instance.navigationSessionAPI + .getNewNavigationSessionEventStream() + .listen((void event) { + listener.call(); + }); + } + /// Sets the event channel listener for the rerouting events. (Android only) /// /// Returns a [StreamSubscription] for rerouting events. @@ -566,6 +601,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 ea4e94fd..47b33bfd 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(); @@ -1315,6 +1313,10 @@ abstract class NavigationSessionEventApi { /// Turn-by-Turn navigation events. void onNavInfo(NavInfoDto navInfo); + + /// Navigation session event. Called when a new navigation + /// session starts with active guidance. + void onNewNavigationSession(); } @HostApi() diff --git a/test/messages_test.g.dart b/test/messages_test.g.dart index fdba8422..f8fe1aab 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(); @@ -5733,6 +5732,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(