diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationPlugin.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationPlugin.kt index 4cacb216..1d1e43ce 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationPlugin.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationPlugin.kt @@ -16,6 +16,7 @@ package com.google.maps.flutter.navigation +import android.app.Application import androidx.lifecycle.Lifecycle import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware @@ -69,9 +70,10 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { AutoMapViewApi.setUp(binding.binaryMessenger, autoViewMessageHandler) autoViewEventApi = AutoViewEventApi(binding.binaryMessenger) - // Setup navigation session manager (instance-level, not singleton) + // Setup navigation session manager + val app = binding.applicationContext as Application val navigationSessionEventApi = NavigationSessionEventApi(binding.binaryMessenger) - sessionManager = GoogleMapsNavigationSessionManager(navigationSessionEventApi) + sessionManager = GoogleMapsNavigationSessionManager(navigationSessionEventApi, app) // Setup platform view factory and its method channel handlers viewEventApi = ViewEventApi(binding.binaryMessenger) 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 08b7f896..7c76f4ce 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 @@ -17,8 +17,8 @@ package com.google.maps.flutter.navigation import android.app.Activity +import android.app.Application import android.location.Location -import android.util.DisplayMetrics import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.Observer @@ -28,8 +28,6 @@ import com.google.android.libraries.navigation.CustomRoutesOptions import com.google.android.libraries.navigation.DisplayOptions import com.google.android.libraries.navigation.NavigationApi import com.google.android.libraries.navigation.NavigationApi.NavigatorListener -import com.google.android.libraries.navigation.NavigationUpdatesOptions -import com.google.android.libraries.navigation.NavigationUpdatesOptions.GeneratedStepImagesType import com.google.android.libraries.navigation.Navigator import com.google.android.libraries.navigation.Navigator.TaskRemovedBehavior import com.google.android.libraries.navigation.RoadSnappedLocationProvider @@ -52,8 +50,10 @@ interface NavigationReadyListener { /** This class handles creation of navigation session and other navigation related tasks. */ class GoogleMapsNavigationSessionManager -constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : - DefaultLifecycleObserver { +constructor( + private val navigationSessionEventApi: NavigationSessionEventApi, + private val application: Application, +) : DefaultLifecycleObserver { companion object { var navigationReadyListener: NavigationReadyListener? = null } @@ -71,7 +71,7 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : null private var speedingListener: SpeedingListener? = null private var weakActivity: WeakReference? = null - private var turnByTurnEventsEnabled: Boolean = false + private var navInfoObserver: Observer? = null private var weakLifecycleOwner: WeakReference? = null private var taskRemovedBehavior: @TaskRemovedBehavior Int = 0 @@ -221,7 +221,7 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : } } - NavigationApi.getNavigator(getActivity(), listener) + NavigationApi.getNavigator(application, listener) } private fun convertNavigatorErrorToFlutterError( @@ -264,34 +264,29 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : return if (roadSnappedLocationProvider != null) { roadSnappedLocationProvider } else { - val application = getActivity().application - if (application != null) { - roadSnappedLocationProvider = NavigationApi.getRoadSnappedLocationProvider(application) - roadSnappedLocationProvider - } else { - throw FlutterError( - "roadSnappedLocationProviderUnavailable", - "Could not get the road snapped location provider, activity not set.", - ) - } + roadSnappedLocationProvider = NavigationApi.getRoadSnappedLocationProvider(application) + roadSnappedLocationProvider } } /** Stops navigation and cleans up internal state of the navigator when it's no longer needed. */ - fun cleanup() { - val navigator = getNavigator() - navigator.stopGuidance() - navigator.clearDestinations() - navigator.simulator.unsetUserLocation() + fun cleanup(resetSession: Boolean = true) { unregisterListeners() - // As unregisterListeners() is removing all listeners, we need to re-register them when - // navigator is re-initialized. This is done in createNavigationSession() method. - GoogleMapsNavigatorHolder.setNavigator(null) - navigationReadyListener?.onNavigationReady(false) + if (resetSession) { + val navigator = getNavigator() + navigator.stopGuidance() + navigator.clearDestinations() + navigator.simulator.unsetUserLocation() + + // As unregisterListeners() is removing all listeners, we need to re-register them when + // navigator is re-initialized. This is done in createNavigationSession() method. + GoogleMapsNavigatorHolder.reset() + navigationReadyListener?.onNavigationReady(false) + } } - private fun unregisterListeners() { + internal fun unregisterListeners() { val navigator = GoogleMapsNavigatorHolder.getNavigator() if (navigator != null) { if (remainingTimeOrDistanceChangedListener != null) { @@ -324,7 +319,7 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : if (roadSnappedLocationListener != null) { disableRoadSnappedLocationUpdates() } - if (turnByTurnEventsEnabled) { + if (navInfoObserver != null) { disableTurnByTurnNavigationEvents() } } @@ -557,7 +552,7 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : * @return true if the terms have been accepted by the user, and false otherwise. */ fun areTermsAccepted(): Boolean { - return NavigationApi.areTermsAccepted(getActivity().application) + return NavigationApi.areTermsAccepted(application) } /** @@ -566,7 +561,7 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : */ fun resetTermsAccepted() { try { - NavigationApi.resetTermsAccepted(getActivity().application) + NavigationApi.resetTermsAccepted(application) } catch (error: IllegalStateException) { throw FlutterError( "termsResetNotAllowed", @@ -690,63 +685,36 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : @Throws(FlutterError::class) fun enableTurnByTurnNavigationEvents(numNextStepsToPreview: Int) { - val lifeCycleOwner: LifecycleOwner? = weakLifecycleOwner?.get() - if (!turnByTurnEventsEnabled && lifeCycleOwner != null) { - - /// DisplayMetrics is required to be set for turn-by-turn updates. - /// But not used as image generation is disabled. - val displayMetrics = DisplayMetrics() - displayMetrics.density = 2.0f - - // Configure options for navigation updates. - val options = - NavigationUpdatesOptions.builder() - .setNumNextStepsToPreview(numNextStepsToPreview) - .setGeneratedStepImagesType(GeneratedStepImagesType.NONE) - .setDisplayMetrics(displayMetrics) - .build() - - // Attempt to register the service for navigation updates. + if (navInfoObserver == null) { + // Register the service centrally (if not already registered) val success = - getNavigator() - .registerServiceForNavUpdates( - getActivity().packageName, - GoogleMapsNavigationNavUpdatesService::class.java.name, - options, - ) + GoogleMapsNavigatorHolder.registerTurnByTurnService(application, numNextStepsToPreview) - if (success) { - val navInfoObserver: Observer = Observer { navInfo -> - navigationSessionEventApi.onNavInfo(Convert.convertNavInfo(navInfo)) {} - } - GoogleMapsNavigationNavUpdatesService.navInfoLiveData.observe( - lifeCycleOwner, - navInfoObserver, - ) - turnByTurnEventsEnabled = true - } else { + if (!success) { throw FlutterError( "turnByTurnServiceError", "Error while registering turn-by-turn updates service.", ) } + + // Create observer for this session manager + navInfoObserver = Observer { navInfo -> + navigationSessionEventApi.onNavInfo(Convert.convertNavInfo(navInfo)) {} + } + + // Add observer using observeForever (works without lifecycle owner) + GoogleMapsNavigatorHolder.addNavInfoObserver(navInfoObserver!!) } } @Throws(FlutterError::class) fun disableTurnByTurnNavigationEvents() { - val lifeCycleOwner: LifecycleOwner? = weakLifecycleOwner?.get() - if (turnByTurnEventsEnabled && lifeCycleOwner != null) { - GoogleMapsNavigationNavUpdatesService.navInfoLiveData.removeObservers(lifeCycleOwner) - val success = getNavigator().unregisterServiceForNavUpdates() - if (success) { - turnByTurnEventsEnabled = false - } else { - throw FlutterError( - "turnByTurnServiceError", - "Error while unregistering turn-by-turn updates service.", - ) - } + if (navInfoObserver != null) { + GoogleMapsNavigatorHolder.removeNavInfoObserver(navInfoObserver!!) + navInfoObserver = null + + // Note: Service will only be unregistered when all observers are removed + GoogleMapsNavigatorHolder.unregisterTurnByTurnService() } } 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 ef233b07..16db8d8b 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 @@ -37,8 +37,8 @@ class GoogleMapsNavigationSessionMessageHandler( GoogleNavigatorInitializationState.INITIALIZED } - override fun cleanup() { - sessionManager.cleanup() + override fun cleanup(resetSession: Boolean) { + sessionManager.cleanup(resetSession) } override fun showTermsAndConditionsDialog( 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 b851758f..37371160 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 @@ -1,6 +1,28 @@ +/* + * Copyright 2025 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. + */ + package com.google.maps.flutter.navigation +import android.app.Application +import android.util.DisplayMetrics +import androidx.lifecycle.Observer +import com.google.android.libraries.mapsplatform.turnbyturn.model.NavInfo import com.google.android.libraries.navigation.NavigationApi +import com.google.android.libraries.navigation.NavigationUpdatesOptions +import com.google.android.libraries.navigation.NavigationUpdatesOptions.GeneratedStepImagesType import com.google.android.libraries.navigation.Navigator /** @@ -18,6 +40,10 @@ object GoogleMapsNavigatorHolder { private var initializationState = GoogleNavigatorInitializationState.NOT_INITIALIZED private val initializationCallbacks = mutableListOf() + // Turn-by-turn navigation service management + private var turnByTurnServiceRegistered = false + private val navInfoObservers = mutableListOf>() + @Synchronized fun getNavigator(): Navigator? = navigator @Synchronized @@ -51,8 +77,79 @@ object GoogleMapsNavigatorHolder { return callbacks } + @Synchronized + fun registerTurnByTurnService(application: Application, numNextStepsToPreview: Int): Boolean { + val nav = navigator ?: return false + + if (!turnByTurnServiceRegistered) { + // DisplayMetrics is required to be set for turn-by-turn updates. + // But not used as image generation is disabled. + val displayMetrics = DisplayMetrics() + displayMetrics.density = 2.0f + + val options = + NavigationUpdatesOptions.builder() + .setNumNextStepsToPreview(numNextStepsToPreview) + .setGeneratedStepImagesType(GeneratedStepImagesType.NONE) + .setDisplayMetrics(displayMetrics) + .build() + + val success = + nav.registerServiceForNavUpdates( + application.packageName, + GoogleMapsNavigationNavUpdatesService::class.java.name, + options, + ) + + if (success) { + turnByTurnServiceRegistered = true + } + return success + } + return true // Already registered + } + + @Synchronized + fun addNavInfoObserver(observer: Observer) { + if (!navInfoObservers.contains(observer)) { + navInfoObservers.add(observer) + GoogleMapsNavigationNavUpdatesService.navInfoLiveData.observeForever(observer) + } + } + + @Synchronized + fun removeNavInfoObserver(observer: Observer) { + if (navInfoObservers.remove(observer)) { + GoogleMapsNavigationNavUpdatesService.navInfoLiveData.removeObserver(observer) + } + } + + @Synchronized + fun unregisterTurnByTurnService(): Boolean { + val nav = navigator ?: return false + + if (turnByTurnServiceRegistered && navInfoObservers.isEmpty()) { + val success = nav.unregisterServiceForNavUpdates() + if (success) { + turnByTurnServiceRegistered = false + } + return success + } + return true + } + @Synchronized fun reset() { + // Clean up turn-by-turn service + if (turnByTurnServiceRegistered) { + for (observer in navInfoObservers.toList()) { + GoogleMapsNavigationNavUpdatesService.navInfoLiveData.removeObserver(observer) + } + navInfoObservers.clear() + navigator?.unregisterServiceForNavUpdates() + turnByTurnServiceRegistered = false + } + navigator = null initializationState = GoogleNavigatorInitializationState.NOT_INITIALIZED initializationCallbacks.clear() 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..90cd57c8 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 @@ -5702,7 +5702,7 @@ interface NavigationSessionApi { fun isInitialized(): Boolean - fun cleanup() + fun cleanup(resetSession: Boolean) fun showTermsAndConditionsDialog( title: String, @@ -5862,10 +5862,12 @@ interface NavigationSessionApi { codec, ) if (api != null) { - channel.setMessageHandler { _, reply -> + channel.setMessageHandler { message, reply -> + val args = message as List + val resetSessionArg = args[0] as Boolean val wrapped: List = try { - api.cleanup() + api.cleanup(resetSessionArg) listOf(null) } catch (exception: Throwable) { MessagesPigeonUtils.wrapError(exception) diff --git a/example/integration_test/t09_isolates_test.dart b/example/integration_test/t09_isolates_test.dart new file mode 100644 index 00000000..f53b9993 --- /dev/null +++ b/example/integration_test/t09_isolates_test.dart @@ -0,0 +1,100 @@ +// 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. + +// This test file validates that the Google Maps Navigation plugin works +// correctly with Dart isolates and background execution, including testing +// that multiple GoogleMapsNavigationSessionManager instances can share +// the same Navigator through native implementations. + +import 'dart:isolate'; +import 'package:flutter/services.dart'; +import 'shared.dart'; + +void main() { + setUpAll(() async { + // No special setup needed for flutter_background_service in tests + }); + + patrol( + 'Test GoogleMapsNavigator.getNavSDKVersion() in multiple background isolates', + (PatrolIntegrationTester $) async { + final RootIsolateToken rootIsolateToken = RootIsolateToken.instance!; + const int numIsolates = 3; + final List receivePorts = []; + + for (int i = 0; i < numIsolates; i++) { + final ReceivePort receivePort = ReceivePort(); + receivePorts.add(receivePort); + + await Isolate.spawn( + _isolateVersionCheckMain, + _IsolateData( + rootIsolateToken: rootIsolateToken, + sendPort: receivePort.sendPort, + ), + ); + } + + final List<_IsolateResult> results = []; + for (final receivePort in receivePorts) { + final dynamic result = await receivePort.first; + expect(result, isA<_IsolateResult>()); + results.add(result as _IsolateResult); + } + + for (int i = 0; i < results.length; i++) { + expect( + results[i].error, + isNull, + reason: 'Isolate $i should not throw an error', + ); + expect(results[i].version, isNotNull); + expect(results[i].version!.length, greaterThan(0)); + } + + final String firstVersion = results[0].version!; + for (int i = 1; i < results.length; i++) { + expect( + results[i].version, + equals(firstVersion), + reason: 'All isolates should return the same SDK version', + ); + } + }, + ); +} + +class _IsolateData { + _IsolateData({required this.rootIsolateToken, required this.sendPort}); + + final RootIsolateToken rootIsolateToken; + final SendPort sendPort; +} + +class _IsolateResult { + _IsolateResult({this.version, this.error}); + + final String? version; + final String? error; +} + +Future _isolateVersionCheckMain(_IsolateData data) async { + try { + BackgroundIsolateBinaryMessenger.ensureInitialized(data.rootIsolateToken); + final String version = await GoogleMapsNavigator.getNavSDKVersion(); + data.sendPort.send(_IsolateResult(version: version)); + } catch (e) { + data.sendPort.send(_IsolateResult(error: e.toString())); + } +} 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..9dbc7d97 100644 --- a/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionManager.swift +++ b/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionManager.swift @@ -148,16 +148,20 @@ class GoogleMapsNavigationSessionManager: NSObject { _session?.navigator != nil } - func cleanup() throws { + func cleanup(resetSession: Bool = true) throws { if _session == nil { throw GoogleMapsNavigationSessionManagerError.sessionNotInitialized } - _session?.locationSimulator?.stopSimulation() - _session?.navigator?.clearDestinations() + _session?.roadSnappedLocationProvider?.remove(self) - _session?.navigator?.isGuidanceActive = false - _session?.isStarted = false - _session = nil + + if resetSession { + _session?.locationSimulator?.stopSimulation() + _session?.navigator?.clearDestinations() + _session?.navigator?.isGuidanceActive = false + _session?.isStarted = false + _session = nil + } } func attachNavigationSessionToMapView(mapId: Int64) throws { 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..8d3db8cf 100644 --- a/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionMessageHandler.swift +++ b/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionMessageHandler.swift @@ -80,8 +80,8 @@ class GoogleMapsNavigationSessionMessageHandler: NavigationSessionApi { GoogleMapsNavigationSessionManager.shared.isInitialized() } - func cleanup() throws { - try GoogleMapsNavigationSessionManager.shared.cleanup() + func cleanup(resetSession: Bool) throws { + try GoogleMapsNavigationSessionManager.shared.cleanup(resetSession: resetSession) } /// Navigation actions 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..d82bb8ad 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 @@ -4867,7 +4867,7 @@ protocol NavigationSessionApi { abnormalTerminationReportingEnabled: Bool, behavior: TaskRemovedBehaviorDto, completion: @escaping (Result) -> Void) func isInitialized() throws -> Bool - func cleanup() throws + func cleanup(resetSession: Bool) throws func showTermsAndConditionsDialog( title: String, companyName: String, shouldOnlyShowDriverAwarenessDisclaimer: Bool, completion: @escaping (Result) -> Void) @@ -4972,9 +4972,11 @@ class NavigationSessionApiSetup { "dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.cleanup\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { - cleanupChannel.setMessageHandler { _, reply in + cleanupChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let resetSessionArg = args[0] as! Bool do { - try api.cleanup() + try api.cleanup(resetSession: resetSessionArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) diff --git a/lib/src/method_channel/messages.g.dart b/lib/src/method_channel/messages.g.dart index 614725e8..9ad8f4e9 100644 --- a/lib/src/method_channel/messages.g.dart +++ b/lib/src/method_channel/messages.g.dart @@ -6693,7 +6693,7 @@ class NavigationSessionApi { } } - Future cleanup() async { + Future cleanup(bool resetSession) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.cleanup$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -6702,7 +6702,9 @@ class NavigationSessionApi { pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [resetSession], + ); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { diff --git a/lib/src/method_channel/session_api.dart b/lib/src/method_channel/session_api.dart index 4cde5fed..bff7a621 100644 --- a/lib/src/method_channel/session_api.dart +++ b/lib/src/method_channel/session_api.dart @@ -86,9 +86,17 @@ class NavigationSessionAPIImpl { } /// Cleanup navigation session. - Future cleanup() async { + /// + /// If [resetSession] is true, clears listeners, any existing route waypoints + /// and stops ongoing navigation guidance and simulation. + /// + /// If [resetSession] is false, only unregisters native event listeners without + /// clearing the navigation session. This is useful for background/foreground + /// service implementations where you want to stop receiving events but keep + /// the navigation session active. + Future cleanup({bool resetSession = true}) async { try { - return await _sessionApi.cleanup(); + return await _sessionApi.cleanup(resetSession); } on PlatformException catch (e) { switch (e.code) { case 'sessionNotInitialized': diff --git a/lib/src/navigator/google_navigation_flutter_navigator.dart b/lib/src/navigator/google_navigation_flutter_navigator.dart index 2bda2f25..59428c4b 100644 --- a/lib/src/navigator/google_navigation_flutter_navigator.dart +++ b/lib/src/navigator/google_navigation_flutter_navigator.dart @@ -441,17 +441,24 @@ class GoogleMapsNavigator { /// Cleans up the navigation session. /// - /// Cleans up the navigator's internal state, clearing - /// listeners, any existing route waypoints and stopping ongoing - /// navigation guidance and simulation. - /// - /// On iOS the session is fully deleted and needs to be recreated - /// by calling [GoogleMapsNavigator.initializeNavigationSession]. - /// - /// On Android the session is cleaned up, but never destroyed after the - /// first initialization. - static Future cleanup() async { - await GoogleMapsNavigationPlatform.instance.navigationSessionAPI.cleanup(); + /// By default ([resetSession] is true), cleans up the navigator's internal + /// state, clearing listeners, any existing route waypoints and stopping + /// ongoing navigation guidance and simulation. + /// + /// When [resetSession] is set to false, only unregisters native event + /// listeners without stopping guidance or clearing destinations. This is + /// useful for background/foreground service implementations where you want to + /// stop receiving events but keep the navigation session active. + /// + /// The session is fully deleted when [resetSession] is true and needs to be + /// recreated by calling [GoogleMapsNavigator.initializeNavigationSession]. + /// + /// Note: When [resetSession] is false, you'll need to re-enable listeners + /// by calling [initializeNavigationSession] again to resume receiving events. + static Future cleanup({bool resetSession = true}) async { + await GoogleMapsNavigationPlatform.instance.navigationSessionAPI.cleanup( + resetSession: resetSession, + ); } /// Shows terms and conditions dialog. diff --git a/pigeons/messages.dart b/pigeons/messages.dart index ea4e94fd..9de0342b 100644 --- a/pigeons/messages.dart +++ b/pigeons/messages.dart @@ -1224,7 +1224,7 @@ abstract class NavigationSessionApi { TaskRemovedBehaviorDto behavior, ); bool isInitialized(); - void cleanup(); + void cleanup(bool resetSession); @async bool showTermsAndConditionsDialog( String title, diff --git a/test/google_navigation_flutter_test.mocks.dart b/test/google_navigation_flutter_test.mocks.dart index 2673750d..c5916b3e 100644 --- a/test/google_navigation_flutter_test.mocks.dart +++ b/test/google_navigation_flutter_test.mocks.dart @@ -105,8 +105,8 @@ class MockTestNavigationSessionApi extends _i1.Mock as bool); @override - void cleanup() => super.noSuchMethod( - Invocation.method(#cleanup, []), + void cleanup(bool? resetSession) => super.noSuchMethod( + Invocation.method(#cleanup, [resetSession]), returnValueForMissingStub: null, ); diff --git a/test/messages_test.g.dart b/test/messages_test.g.dart index fdba8422..f94c7885 100644 --- a/test/messages_test.g.dart +++ b/test/messages_test.g.dart @@ -4975,7 +4975,7 @@ abstract class TestNavigationSessionApi { bool isInitialized(); - void cleanup(); + void cleanup(bool resetSession); Future showTermsAndConditionsDialog( String title, @@ -5163,8 +5163,18 @@ abstract class TestNavigationSessionApi { .setMockDecodedMessageHandler(pigeonVar_channel, ( Object? message, ) async { + assert( + message != null, + 'Argument for dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.cleanup was null.', + ); + final List args = (message as List?)!; + final bool? arg_resetSession = (args[0] as bool?); + assert( + arg_resetSession != null, + 'Argument for dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.cleanup was null, expected non-null bool.', + ); try { - api.cleanup(); + api.cleanup(arg_resetSession!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e);