From 0b10c943dbd4fa1998f34ab3989b5e5549d1f6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20V=C3=A4limaa?= Date: Thu, 30 Oct 2025 15:41:39 +0800 Subject: [PATCH 01/10] Initial conversion of navigation session manager to instance from singleton on Android --- .../navigation/AndroidAutoBaseScreen.kt | 2 +- .../GoogleMapsNavigationInspectorHandler.kt | 8 +- .../navigation/GoogleMapsNavigationPlugin.kt | 25 +- .../GoogleMapsNavigationSessionManager.kt | 232 ++++++++++-------- ...ogleMapsNavigationSessionMessageHandler.kt | 6 +- .../navigation/GoogleMapsNavigationView.kt | 3 +- 6 files changed, 163 insertions(+), 113 deletions(-) diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt index 7446158b..874e1ee8 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt @@ -57,7 +57,7 @@ open class AndroidAutoBaseScreen(carContext: CarContext) : GoogleMapsNavigationSessionManager.navigationReadyListener = this mIsNavigationReady = try { - GoogleMapsNavigationSessionManager.getInstance().isInitialized() + GoogleMapsNavigationPlugin.getInstance()?.sessionManager?.isInitialized() ?: false } catch (exception: RuntimeException) { // If GoogleMapsNavigationSessionManager is not initialized navigation is not ready. false diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt index 6a67bde5..372c4be9 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt @@ -14,10 +14,12 @@ package com.google.maps.flutter.navigation -class GoogleMapsNavigationInspectorHandler(private val viewRegistry: GoogleMapsViewRegistry) : - NavigationInspector { +class GoogleMapsNavigationInspectorHandler( + private val viewRegistry: GoogleMapsViewRegistry, + private val sessionManager: GoogleMapsNavigationSessionManager +) : NavigationInspector { private fun manager(): GoogleMapsNavigationSessionManager { - return GoogleMapsNavigationSessionManager.getInstance() + return sessionManager } override fun isViewAttachedToSession(viewId: Long): Boolean { 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 78866d8c..feff3087 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 @@ -40,6 +40,9 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { private var viewMessageHandler: GoogleMapsViewMessageHandler? = null private var imageRegistryMessageHandler: GoogleMapsImageRegistryMessageHandler? = null private var autoViewMessageHandler: GoogleMapsAutoViewMessageHandler? = null + + // Instance-level session manager instead of singleton + internal var sessionManager: GoogleMapsNavigationSessionManager? = null private var lifecycle: Lifecycle? = null @@ -67,9 +70,15 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { AutoMapViewApi.setUp(binding.binaryMessenger, autoViewMessageHandler) autoViewEventApi = AutoViewEventApi(binding.binaryMessenger) - // Setup navigation session manager and its method channel handlers - GoogleMapsNavigationSessionManager.createInstance(binding.binaryMessenger) - val inspectorHandler = GoogleMapsNavigationInspectorHandler(viewRegistry!!) + // Setup navigation session manager (instance-level, not singleton) + val navigationSessionEventApi = NavigationSessionEventApi(binding.binaryMessenger) + sessionManager = GoogleMapsNavigationSessionManager(navigationSessionEventApi) + + // Setup navigation session message handler with this instance's session manager + val sessionMessageHandler = GoogleMapsNavigationSessionMessageHandler(sessionManager!!) + NavigationSessionApi.setUp(binding.binaryMessenger, sessionMessageHandler) + + val inspectorHandler = GoogleMapsNavigationInspectorHandler(viewRegistry!!, sessionManager!!) NavigationInspector.setUp(binding.binaryMessenger, inspectorHandler) } @@ -80,10 +89,10 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { AutoMapViewApi.setUp(binding.binaryMessenger, null) NavigationInspector.setUp(binding.binaryMessenger, null) - GoogleMapsNavigationSessionManager.destroyInstance() binding.applicationContext.unregisterComponentCallbacks(viewRegistry) // Cleanup references + sessionManager = null viewRegistry = null viewMessageHandler = null imageRegistryMessageHandler = null @@ -98,18 +107,18 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding).also { lc -> viewRegistry?.let(lc::addObserver) - GoogleMapsNavigationSessionManager.getInstanceOrNull()?.let(lc::addObserver) + sessionManager?.let(lc::addObserver) } - GoogleMapsNavigationSessionManager.getInstanceOrNull()?.onActivityCreated(binding.activity) + sessionManager?.onActivityCreated(binding.activity) } private fun detachActivity(forConfigChange: Boolean) { lifecycle?.let { lc -> viewRegistry?.let(lc::removeObserver) - GoogleMapsNavigationSessionManager.getInstanceOrNull()?.let(lc::removeObserver) + sessionManager?.let(lc::removeObserver) } - GoogleMapsNavigationSessionManager.getInstanceOrNull()?.onActivityDestroyed(forConfigChange) + sessionManager?.onActivityDestroyed(forConfigChange) lifecycle = null } 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..e0f35ae3 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 @@ -51,62 +51,60 @@ interface NavigationReadyListener { fun onNavigationReady(ready: Boolean) } +/** + * Singleton holder for the shared Navigator instance. + * Multiple GoogleMapsNavigationSessionManager instances share the same Navigator. + */ +object SharedNavigatorHolder { + @Volatile + private var navigator: Navigator? = null + private var initializationInProgress = false + private val initializationCallbacks = mutableListOf() + + @Synchronized + fun getNavigator(): Navigator? = navigator + + @Synchronized + fun setNavigator(nav: Navigator?) { + navigator = nav + } + + @Synchronized + fun isInitializationInProgress(): Boolean = initializationInProgress + + @Synchronized + fun setInitializationInProgress(inProgress: Boolean) { + initializationInProgress = inProgress + } + + @Synchronized + fun addInitializationCallback(callback: NavigatorListener) { + initializationCallbacks.add(callback) + } + + @Synchronized + fun getAndClearInitializationCallbacks(): List { + val callbacks = initializationCallbacks.toList() + initializationCallbacks.clear() + return callbacks + } + + @Synchronized + fun reset() { + navigator = null + initializationInProgress = false + initializationCallbacks.clear() + } +} + /** This class handles creation of navigation session and other navigation related tasks. */ class GoogleMapsNavigationSessionManager -private constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : +constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : DefaultLifecycleObserver { companion object { - private var instance: GoogleMapsNavigationSessionManager? = null var navigationReadyListener: NavigationReadyListener? = null - - /** - * Create new GoogleMapsNavigationSessionManager instance. Does nothing if instance is already - * created. - * - * @param binaryMessenger BinaryMessenger to use for API setup. - */ - @Synchronized - fun createInstance(binaryMessenger: BinaryMessenger) { - if (instance != null) { - return - } - - val sessionMessageHandler = GoogleMapsNavigationSessionMessageHandler() - NavigationSessionApi.setUp(binaryMessenger, sessionMessageHandler) - val navigationSessionEventApi = NavigationSessionEventApi(binaryMessenger) - instance = GoogleMapsNavigationSessionManager(navigationSessionEventApi) - } - - /** - * Stop all navigation related tasks and destroy [GoogleMapsNavigationSessionManager] instance. - */ - @Synchronized - fun destroyInstance() { - // Stop all navigation related tasks. - instance = null - } - - /** - * Get instance that was previously created - * - * @return [GoogleMapsNavigationSessionManager] instance. - */ - @Synchronized - fun getInstance(): GoogleMapsNavigationSessionManager { - if (instance == null) { - throw RuntimeException("Instance not created, create with createInstance()") - } - return instance!! - } - - /** Get instance if available, or null if not created. */ - @Synchronized - fun getInstanceOrNull(): GoogleMapsNavigationSessionManager? { - return instance - } } - private var navigator: Navigator? = null private var isNavigationSessionInitialized = false private var arrivalListener: Navigator.ArrivalListener? = null private var routeChangedListener: Navigator.RouteChangedListener? = null @@ -160,8 +158,9 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven @Throws(FlutterError::class) fun getNavigator(): Navigator { - if (navigator != null) { - return navigator!! + val nav = SharedNavigatorHolder.getNavigator() + if (nav != null) { + return nav } else { throw FlutterError( "sessionNotInitialized", @@ -173,7 +172,7 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven // Expose the navigator to the google_maps_driver side. // DriverApi initialization requires navigator. fun getNavigatorWithoutError(): Navigator? { - return navigator + return SharedNavigatorHolder.getNavigator() } /** Creates Navigator instance. */ @@ -182,7 +181,7 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven behavior: TaskRemovedBehaviorDto, callback: (Result) -> Unit, ) { - if (navigator != null) { + if (SharedNavigatorHolder.getNavigator() != null) { // Navigator is already initialized, just re-register listeners. registerNavigationListeners() isNavigationSessionInitialized = true @@ -190,6 +189,26 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven callback(Result.success(Unit)) return } + + // Check if initialization is already in progress by another instance + if (SharedNavigatorHolder.isInitializationInProgress()) { + // Add this callback to the queue to be called when initialization completes + val queuedListener = object : NavigatorListener { + override fun onNavigatorReady(newNavigator: Navigator) { + registerNavigationListeners() + isNavigationSessionInitialized = true + navigationReadyListener?.onNavigationReady(true) + callback(Result.success(Unit)) + } + + override fun onError(@NavigationApi.ErrorCode errorCode: Int) { + callback(Result.failure(convertNavigatorErrorToFlutterError(errorCode))) + } + } + SharedNavigatorHolder.addInitializationCallback(queuedListener) + return + } + taskRemovedBehavior = Convert.taskRemovedBehaviorDtoToTaskRemovedBehavior(behavior) // Align API behavior with iOS: @@ -209,66 +228,83 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven // Enable or disable abnormal termination reporting. NavigationApi.setAbnormalTerminationReportingEnabled(abnormalTerminationReportingEnabled) + // Mark initialization as in progress + SharedNavigatorHolder.setInitializationInProgress(true) + val listener = object : NavigatorListener { override fun onNavigatorReady(newNavigator: Navigator) { - navigator = newNavigator - navigator?.setTaskRemovedBehavior(taskRemovedBehavior) + SharedNavigatorHolder.setNavigator(newNavigator) + newNavigator.setTaskRemovedBehavior(taskRemovedBehavior) registerNavigationListeners() isNavigationSessionInitialized = true navigationReadyListener?.onNavigationReady(true) + + // Mark initialization as complete + SharedNavigatorHolder.setInitializationInProgress(false) + + // Notify all queued callbacks + val queuedCallbacks = SharedNavigatorHolder.getAndClearInitializationCallbacks() + for (queuedCallback in queuedCallbacks) { + queuedCallback.onNavigatorReady(newNavigator) + } + callback(Result.success(Unit)) } override fun onError(@NavigationApi.ErrorCode errorCode: Int) { - // Keep in sync with GoogleMapsNavigationSessionManager.swift - when (errorCode) { - NavigationApi.ErrorCode.NOT_AUTHORIZED -> { - callback( - Result.failure( - FlutterError( - "notAuthorized", - "The session initialization failed, because the required Maps API key is empty or invalid.", - ) - ) - ) - } - NavigationApi.ErrorCode.TERMS_NOT_ACCEPTED -> { - callback( - Result.failure( - FlutterError( - "termsNotAccepted", - "The session initialization failed, because the user has not yet accepted the navigation terms and conditions.", - ) - ) - ) - } - NavigationApi.ErrorCode.NETWORK_ERROR -> { - callback( - Result.failure( - FlutterError( - "networkError", - "The session initialization failed, because there is no working network connection.", - ) - ) - ) - } - NavigationApi.ErrorCode.LOCATION_PERMISSION_MISSING -> { - callback( - Result.failure( - FlutterError( - "locationPermissionMissing", - "The session initialization failed, because the required location permission has not been granted.", - ) - ) - ) - } + SharedNavigatorHolder.setInitializationInProgress(false) + + val error = convertNavigatorErrorToFlutterError(errorCode) + + // Notify all queued callbacks about the error + val queuedCallbacks = SharedNavigatorHolder.getAndClearInitializationCallbacks() + for (queuedCallback in queuedCallbacks) { + queuedCallback.onError(errorCode) } + + callback(Result.failure(error)) } } NavigationApi.getNavigator(getActivity(), listener) } + + private fun convertNavigatorErrorToFlutterError(@NavigationApi.ErrorCode errorCode: Int): FlutterError { + // Keep in sync with GoogleMapsNavigationSessionManager.swift + return when (errorCode) { + NavigationApi.ErrorCode.NOT_AUTHORIZED -> { + FlutterError( + "notAuthorized", + "The session initialization failed, because the required Maps API key is empty or invalid.", + ) + } + NavigationApi.ErrorCode.TERMS_NOT_ACCEPTED -> { + FlutterError( + "termsNotAccepted", + "The session initialization failed, because the user has not yet accepted the navigation terms and conditions.", + ) + } + NavigationApi.ErrorCode.NETWORK_ERROR -> { + FlutterError( + "networkError", + "The session initialization failed, because there is no working network connection.", + ) + } + NavigationApi.ErrorCode.LOCATION_PERMISSION_MISSING -> { + FlutterError( + "locationPermissionMissing", + "The session initialization failed, because the required location permission has not been granted.", + ) + } + else -> { + FlutterError( + "unknownError", + "The session initialization failed with an unknown error.", + ) + } + } + } @Throws(FlutterError::class) private fun getRoadSnappedLocationProvider(): RoadSnappedLocationProvider? { @@ -567,7 +603,7 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven * @return true if session is already created. */ fun isInitialized(): Boolean { - return navigator != null && isNavigationSessionInitialized + return SharedNavigatorHolder.getNavigator() != null && isNavigationSessionInitialized } /** 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..537eeb50 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 @@ -20,9 +20,11 @@ import com.google.android.gms.maps.model.LatLng import com.google.android.libraries.navigation.RoutingOptions import com.google.android.libraries.navigation.SimulationOptions -class GoogleMapsNavigationSessionMessageHandler : NavigationSessionApi { +class GoogleMapsNavigationSessionMessageHandler( + private val sessionManager: GoogleMapsNavigationSessionManager +) : NavigationSessionApi { private fun manager(): GoogleMapsNavigationSessionManager { - return GoogleMapsNavigationSessionManager.getInstance() + return sessionManager } override fun createNavigationSession( diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt index 605c2828..42e0811c 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt @@ -67,7 +67,8 @@ internal constructor( if ( navigationOptions?.navigationUiEnabledPreference == NavigationUIEnabledPreference.AUTOMATIC ) { - val navigatorInitialized = GoogleMapsNavigationSessionManager.getInstance().isInitialized() + val plugin = GoogleMapsNavigationPlugin.getInstance() + val navigatorInitialized = plugin?.sessionManager?.isInitialized() ?: false if (navigatorInitialized) { navigationViewEnabled = true } From 24be348f0a49e8954cba6f52d910f5ab87a0f706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20V=C3=A4limaa?= Date: Thu, 30 Oct 2025 18:55:42 +0800 Subject: [PATCH 02/10] refactoring android navigator instance management --- .../navigation/AndroidAutoBaseScreen.kt | 8 +-- .../GoogleMapsNavigationInspectorHandler.kt | 9 +-- .../navigation/GoogleMapsNavigationPlugin.kt | 17 +++-- .../GoogleMapsNavigationSessionManager.kt | 25 +------ ...ogleMapsNavigationSessionMessageHandler.kt | 69 +++++++++---------- .../navigation/GoogleMapsNavigationView.kt | 10 +-- .../navigation/GoogleMapsViewFactory.kt | 4 +- 7 files changed, 54 insertions(+), 88 deletions(-) diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt index 874e1ee8..ac5335fb 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt @@ -55,13 +55,7 @@ open class AndroidAutoBaseScreen(carContext: CarContext) : private fun initializeNavigationListener() { GoogleMapsNavigationSessionManager.navigationReadyListener = this - mIsNavigationReady = - try { - GoogleMapsNavigationPlugin.getInstance()?.sessionManager?.isInitialized() ?: false - } catch (exception: RuntimeException) { - // If GoogleMapsNavigationSessionManager is not initialized navigation is not ready. - false - } + mIsNavigationReady = SharedNavigatorHolder.getNavigator() != null } private fun initializeSurfaceCallback() { diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt index 372c4be9..dd4a020b 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt @@ -15,17 +15,12 @@ package com.google.maps.flutter.navigation class GoogleMapsNavigationInspectorHandler( - private val viewRegistry: GoogleMapsViewRegistry, - private val sessionManager: GoogleMapsNavigationSessionManager + private val viewRegistry: GoogleMapsViewRegistry ) : NavigationInspector { - private fun manager(): GoogleMapsNavigationSessionManager { - return sessionManager - } - override fun isViewAttachedToSession(viewId: Long): Boolean { /// Is session exists, it's automatically attached to any existing view. if (viewRegistry.getNavigationView(viewId.toInt()) != null) { - return manager().isInitialized() + return SharedNavigatorHolder.getNavigator() != null } return false } 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 feff3087..4a83833c 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 @@ -47,7 +47,10 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { private var lifecycle: Lifecycle? = null override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - instance = this + // Store first instance of the plugin so that Android Auto will get access to the correct object instances. + if (instance == null) { + instance = this + } // Init view registry and its method channel handlers viewRegistry = GoogleMapsViewRegistry() @@ -60,11 +63,6 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { imageRegistryMessageHandler = GoogleMapsImageRegistryMessageHandler(imageRegistry!!) ImageRegistryApi.setUp(binding.binaryMessenger, imageRegistryMessageHandler) - // Setup platform view factory and its method channel handlers - viewEventApi = ViewEventApi(binding.binaryMessenger) - val factory = GoogleMapsViewFactory(viewRegistry!!, viewEventApi!!, imageRegistry!!) - binding.platformViewRegistry.registerViewFactory("google_navigation_flutter", factory) - // Setup auto map view method channel handlers autoViewMessageHandler = GoogleMapsAutoViewMessageHandler(viewRegistry!!) AutoMapViewApi.setUp(binding.binaryMessenger, autoViewMessageHandler) @@ -73,12 +71,17 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { // Setup navigation session manager (instance-level, not singleton) val navigationSessionEventApi = NavigationSessionEventApi(binding.binaryMessenger) sessionManager = GoogleMapsNavigationSessionManager(navigationSessionEventApi) + + // Setup platform view factory and its method channel handlers + viewEventApi = ViewEventApi(binding.binaryMessenger) + val factory = GoogleMapsViewFactory(viewRegistry!!, viewEventApi!!, imageRegistry!!) + binding.platformViewRegistry.registerViewFactory("google_navigation_flutter", factory) // Setup navigation session message handler with this instance's session manager val sessionMessageHandler = GoogleMapsNavigationSessionMessageHandler(sessionManager!!) NavigationSessionApi.setUp(binding.binaryMessenger, sessionMessageHandler) - val inspectorHandler = GoogleMapsNavigationInspectorHandler(viewRegistry!!, sessionManager!!) + val inspectorHandler = GoogleMapsNavigationInspectorHandler(viewRegistry!!) NavigationInspector.setUp(binding.binaryMessenger, inspectorHandler) } 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 e0f35ae3..75ea76a4 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 @@ -105,7 +105,6 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : var navigationReadyListener: NavigationReadyListener? = null } - private var isNavigationSessionInitialized = false private var arrivalListener: Navigator.ArrivalListener? = null private var routeChangedListener: Navigator.RouteChangedListener? = null private var reroutingListener: Navigator.ReroutingListener? = null @@ -169,12 +168,6 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : } } - // Expose the navigator to the google_maps_driver side. - // DriverApi initialization requires navigator. - fun getNavigatorWithoutError(): Navigator? { - return SharedNavigatorHolder.getNavigator() - } - /** Creates Navigator instance. */ fun createNavigationSession( abnormalTerminationReportingEnabled: Boolean, @@ -184,7 +177,6 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : if (SharedNavigatorHolder.getNavigator() != null) { // Navigator is already initialized, just re-register listeners. registerNavigationListeners() - isNavigationSessionInitialized = true navigationReadyListener?.onNavigationReady(true) callback(Result.success(Unit)) return @@ -196,7 +188,6 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : val queuedListener = object : NavigatorListener { override fun onNavigatorReady(newNavigator: Navigator) { registerNavigationListeners() - isNavigationSessionInitialized = true navigationReadyListener?.onNavigationReady(true) callback(Result.success(Unit)) } @@ -237,7 +228,6 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : SharedNavigatorHolder.setNavigator(newNavigator) newNavigator.setTaskRemovedBehavior(taskRemovedBehavior) registerNavigationListeners() - isNavigationSessionInitialized = true navigationReadyListener?.onNavigationReady(true) // Mark initialization as complete @@ -334,13 +324,13 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : // As unregisterListeners() is removing all listeners, we need to re-register them when // navigator is re-initialized. This is done in createNavigationSession() method. - isNavigationSessionInitialized = false + SharedNavigatorHolder.setNavigator(null) navigationReadyListener?.onNavigationReady(false) } private fun unregisterListeners() { - if (isInitialized()) { - val navigator = getNavigator() + val navigator = SharedNavigatorHolder.getNavigator() + if (navigator != null) { if (remainingTimeOrDistanceChangedListener != null) { navigator.removeRemainingTimeOrDistanceChangedListener( remainingTimeOrDistanceChangedListener @@ -597,15 +587,6 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : } } - /** - * Check if navigation session is already created. - * - * @return true if session is already created. - */ - fun isInitialized(): Boolean { - return SharedNavigatorHolder.getNavigator() != null && isNavigationSessionInitialized - } - /** * Wraps [NavigationApi.areTermsAccepted]. See * [Google Navigation SDK for Android](https://developers.google.com/maps/documentation/navigation/android-sdk/reference/com/google/android/libraries/navigation/NavigationApi#areTermsAccepted(android.app.Application)). 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 537eeb50..8343d565 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 @@ -23,24 +23,21 @@ import com.google.android.libraries.navigation.SimulationOptions class GoogleMapsNavigationSessionMessageHandler( private val sessionManager: GoogleMapsNavigationSessionManager ) : NavigationSessionApi { - private fun manager(): GoogleMapsNavigationSessionManager { - return sessionManager - } override fun createNavigationSession( abnormalTerminationReportingEnabled: Boolean, behavior: TaskRemovedBehaviorDto, callback: (Result) -> Unit, ) { - manager().createNavigationSession(abnormalTerminationReportingEnabled, behavior, callback) + sessionManager.createNavigationSession(abnormalTerminationReportingEnabled, behavior, callback) } override fun isInitialized(): Boolean { - return manager().isInitialized() + return SharedNavigatorHolder.getNavigator() != null } override fun cleanup() { - manager().cleanup() + sessionManager.cleanup() } override fun showTermsAndConditionsDialog( @@ -49,7 +46,7 @@ class GoogleMapsNavigationSessionMessageHandler( shouldOnlyShowDriverAwarenessDisclaimer: Boolean, callback: (Result) -> Unit, ) { - manager() + sessionManager .showTermsAndConditionsDialog( title, companyName, @@ -59,27 +56,27 @@ class GoogleMapsNavigationSessionMessageHandler( } override fun areTermsAccepted(): Boolean { - return manager().areTermsAccepted() + return sessionManager.areTermsAccepted() } override fun resetTermsAccepted() { - manager().resetTermsAccepted() + sessionManager.resetTermsAccepted() } override fun getNavSDKVersion(): String { - return manager().getNavSDKVersion() + return sessionManager.getNavSDKVersion() } override fun isGuidanceRunning(): Boolean { - return manager().isGuidanceRunning() + return sessionManager.isGuidanceRunning() } override fun startGuidance() { - manager().startGuidance() + sessionManager.startGuidance() } override fun stopGuidance() { - manager().stopGuidance() + sessionManager.stopGuidance() } override fun setDestinations( @@ -95,7 +92,7 @@ class GoogleMapsNavigationSessionMessageHandler( } else { RoutingOptions() } - manager().setDestinations( + sessionManager.setDestinations( waypoints, routingOptions, displayOptions, @@ -113,11 +110,11 @@ class GoogleMapsNavigationSessionMessageHandler( } override fun clearDestinations() { - manager().clearDestinations() + sessionManager.clearDestinations() } override fun continueToNextDestination(): NavigationWaypointDto? { - val waypoint = manager().continueToNextDestination() + val waypoint = sessionManager.continueToNextDestination() return if (waypoint != null) { Convert.convertWaypointToDto(waypoint) } else { @@ -126,32 +123,32 @@ class GoogleMapsNavigationSessionMessageHandler( } override fun getCurrentTimeAndDistance(): NavigationTimeAndDistanceDto { - val timeAndDistance = manager().getCurrentTimeAndDistance() + val timeAndDistance = sessionManager.getCurrentTimeAndDistance() return Convert.convertTimeAndDistanceToDto(timeAndDistance) } override fun setAudioGuidance(settings: NavigationAudioGuidanceSettingsDto) { val audioGuidanceSettings = Convert.convertAudioGuidanceSettingsToDto(settings) - manager().setAudioGuidance(audioGuidanceSettings) + sessionManager.setAudioGuidance(audioGuidanceSettings) } override fun setSpeedAlertOptions(options: SpeedAlertOptionsDto) { val newOptions = Convert.convertSpeedAlertOptionsFromDto(options) - manager().setSpeedAlertOptions(newOptions) + sessionManager.setSpeedAlertOptions(newOptions) } override fun getRouteSegments(): List { - val routeSegments = manager().getRouteSegments() + val routeSegments = sessionManager.getRouteSegments() return routeSegments.map { Convert.convertRouteSegmentToDto(it) } } override fun getTraveledRoute(): List { - val traveledRoute = manager().getTraveledRoute() + val traveledRoute = sessionManager.getTraveledRoute() return traveledRoute.map { LatLngDto(it.latitude, it.longitude) } } override fun getCurrentRouteSegment(): RouteSegmentDto? { - val currentRouteSegment = manager().getCurrentRouteSegment() + val currentRouteSegment = sessionManager.getCurrentRouteSegment() if (currentRouteSegment != null) { return Convert.convertRouteSegmentToDto(currentRouteSegment) } @@ -159,19 +156,19 @@ class GoogleMapsNavigationSessionMessageHandler( } override fun setUserLocation(location: LatLngDto) { - manager().setUserLocation(LatLng(location.latitude, location.longitude)) + sessionManager.setUserLocation(LatLng(location.latitude, location.longitude)) } override fun removeUserLocation() { - manager().removeUserLocation() + sessionManager.removeUserLocation() } override fun simulateLocationsAlongExistingRoute() { - manager().simulateLocationsAlongExistingRoute() + sessionManager.simulateLocationsAlongExistingRoute() } override fun simulateLocationsAlongExistingRouteWithOptions(options: SimulationOptionsDto) { - manager() + sessionManager .simulateLocationsAlongExistingRouteWithOptions( SimulationOptions().speedMultiplier(options.speedMultiplier.toFloat()) ) @@ -182,7 +179,7 @@ class GoogleMapsNavigationSessionMessageHandler( callback: (Result) -> Unit, ) { val convertedWaypoints = waypoints.map { Convert.convertWaypointFromDto(it) } - manager().simulateLocationsAlongNewRoute(convertedWaypoints) { + sessionManager.simulateLocationsAlongNewRoute(convertedWaypoints) { if (it.isSuccess) { callback(Result.success(Convert.convertRouteStatusToDto(it.getOrThrow()))) } else { @@ -200,7 +197,7 @@ class GoogleMapsNavigationSessionMessageHandler( callback: (Result) -> Unit, ) { val convertedWaypoints = waypoints.map { Convert.convertWaypointFromDto(it) } - manager().simulateLocationsAlongNewRouteWithRoutingOptions( + sessionManager.simulateLocationsAlongNewRouteWithRoutingOptions( convertedWaypoints, Convert.convertRoutingOptionsFromDto(routingOptions), ) { @@ -222,7 +219,7 @@ class GoogleMapsNavigationSessionMessageHandler( callback: (Result) -> Unit, ) { val convertedWaypoints = waypoints.map { Convert.convertWaypointFromDto(it) } - manager().simulateLocationsAlongNewRouteWithRoutingAndSimulationOptions( + sessionManager.simulateLocationsAlongNewRouteWithRoutingAndSimulationOptions( convertedWaypoints, Convert.convertRoutingOptionsFromDto(routingOptions), SimulationOptions().speedMultiplier(simulationOptions.speedMultiplier.toFloat()), @@ -239,11 +236,11 @@ class GoogleMapsNavigationSessionMessageHandler( } override fun pauseSimulation() { - manager().pauseSimulation() + sessionManager.pauseSimulation() } override fun resumeSimulation() { - manager().resumeSimulation() + sessionManager.resumeSimulation() } override fun allowBackgroundLocationUpdates(allow: Boolean) { @@ -251,26 +248,26 @@ class GoogleMapsNavigationSessionMessageHandler( } override fun enableRoadSnappedLocationUpdates() { - manager().enableRoadSnappedLocationUpdates() + sessionManager.enableRoadSnappedLocationUpdates() } override fun disableRoadSnappedLocationUpdates() { - manager().disableRoadSnappedLocationUpdates() + sessionManager.disableRoadSnappedLocationUpdates() } override fun enableTurnByTurnNavigationEvents(numNextStepsToPreview: Long?) { - manager().enableTurnByTurnNavigationEvents(numNextStepsToPreview?.toInt() ?: Int.MAX_VALUE) + sessionManager.enableTurnByTurnNavigationEvents(numNextStepsToPreview?.toInt() ?: Int.MAX_VALUE) } override fun disableTurnByTurnNavigationEvents() { - manager().disableTurnByTurnNavigationEvents() + sessionManager.disableTurnByTurnNavigationEvents() } override fun registerRemainingTimeOrDistanceChangedListener( remainingTimeThresholdSeconds: Long, remainingDistanceThresholdMeters: Long, ) { - manager() + sessionManager .registerRemainingTimeOrDistanceChangedListener( remainingTimeThresholdSeconds, remainingDistanceThresholdMeters, diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt index 42e0811c..d999e0c7 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt @@ -32,7 +32,7 @@ internal constructor( viewId: Int, private val viewRegistry: GoogleMapsViewRegistry, viewEventApi: ViewEventApi, - private val imageRegistry: ImageRegistry, + private val imageRegistry: ImageRegistry ) : PlatformView, GoogleMapsBaseMapView(viewId, mapOptions, viewEventApi, imageRegistry) { private val _navigationView: NavigationView = NavigationView(context, mapOptions.googleMapOptions) @@ -65,13 +65,9 @@ internal constructor( // Initialize navigation view with given navigation view options var navigationViewEnabled = false if ( - navigationOptions?.navigationUiEnabledPreference == NavigationUIEnabledPreference.AUTOMATIC + navigationOptions?.navigationUiEnabledPreference == NavigationUIEnabledPreference.AUTOMATIC && SharedNavigatorHolder.getNavigator() != null ) { - val plugin = GoogleMapsNavigationPlugin.getInstance() - val navigatorInitialized = plugin?.sessionManager?.isInitialized() ?: false - if (navigatorInitialized) { - navigationViewEnabled = true - } + navigationViewEnabled = true } _navigationView.isNavigationUiEnabled = navigationViewEnabled diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsViewFactory.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsViewFactory.kt index 9e3194bf..e6e9f6a1 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsViewFactory.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsViewFactory.kt @@ -24,7 +24,7 @@ import java.util.Objects class GoogleMapsViewFactory( private val viewRegistry: GoogleMapsViewRegistry, private val viewEventApi: ViewEventApi, - private val imageRegistry: ImageRegistry, + private val imageRegistry: ImageRegistry ) : PlatformViewFactory(ViewCreationApi.codec) { override fun create(context: Context, viewId: Int, args: Any?): PlatformView { val params = Objects.requireNonNull(args as ViewCreationOptionsDto) @@ -39,7 +39,7 @@ class GoogleMapsViewFactory( viewId, viewRegistry, viewEventApi, - imageRegistry, + imageRegistry ) } else { return GoogleMapView(context, mapOptions, viewId, viewEventApi, viewRegistry, imageRegistry) From 9ec67700ad0fefd38d70d3aa63e5196da31cfc72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20V=C3=A4limaa?= Date: Fri, 31 Oct 2025 12:03:47 +0800 Subject: [PATCH 03/10] Adjustments to navigator initialization logic --- .../GoogleMapsNavigationSessionManager.kt | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 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 75ea76a4..50476096 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 @@ -55,10 +55,16 @@ interface NavigationReadyListener { * Singleton holder for the shared Navigator instance. * Multiple GoogleMapsNavigationSessionManager instances share the same Navigator. */ +enum class GoogleNavigatorInitializationState { + NOT_INITIALIZED, + INITIALIZING, + INITIALIZED, +} + object SharedNavigatorHolder { @Volatile private var navigator: Navigator? = null - private var initializationInProgress = false + var initializationState = GoogleNavigatorInitializationState.NOT_INITIALIZED private val initializationCallbacks = mutableListOf() @Synchronized @@ -67,14 +73,19 @@ object SharedNavigatorHolder { @Synchronized fun setNavigator(nav: Navigator?) { navigator = nav + initializationState = if (nav != null) { + GoogleNavigatorInitializationState.INITIALIZED + } else { + GoogleNavigatorInitializationState.NOT_INITIALIZED + } } @Synchronized - fun isInitializationInProgress(): Boolean = initializationInProgress + fun getInitializationState(): GoogleNavigatorInitializationState = initializationState @Synchronized - fun setInitializationInProgress(inProgress: Boolean) { - initializationInProgress = inProgress + fun setInitializationState(state: GoogleNavigatorInitializationState) { + initializationState = state } @Synchronized @@ -92,7 +103,7 @@ object SharedNavigatorHolder { @Synchronized fun reset() { navigator = null - initializationInProgress = false + initializationState = GoogleNavigatorInitializationState.NOT_INITIALIZED initializationCallbacks.clear() } } @@ -174,7 +185,9 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : behavior: TaskRemovedBehaviorDto, callback: (Result) -> Unit, ) { - if (SharedNavigatorHolder.getNavigator() != null) { + val currentState = SharedNavigatorHolder.getInitializationState() + + if (currentState == GoogleNavigatorInitializationState.INITIALIZED) { // Navigator is already initialized, just re-register listeners. registerNavigationListeners() navigationReadyListener?.onNavigationReady(true) @@ -183,7 +196,7 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : } // Check if initialization is already in progress by another instance - if (SharedNavigatorHolder.isInitializationInProgress()) { + if (currentState == GoogleNavigatorInitializationState.INITIALIZING) { // Add this callback to the queue to be called when initialization completes val queuedListener = object : NavigatorListener { override fun onNavigatorReady(newNavigator: Navigator) { @@ -220,19 +233,20 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : NavigationApi.setAbnormalTerminationReportingEnabled(abnormalTerminationReportingEnabled) // Mark initialization as in progress - SharedNavigatorHolder.setInitializationInProgress(true) + SharedNavigatorHolder.setInitializationState(GoogleNavigatorInitializationState.INITIALIZING) val listener = object : NavigatorListener { override fun onNavigatorReady(newNavigator: Navigator) { + if (SharedNavigatorHolder.initializationState != GoogleNavigatorInitializationState.INITIALIZING) { + SharedNavigatorHolder.setNavigator(null); + return + } SharedNavigatorHolder.setNavigator(newNavigator) newNavigator.setTaskRemovedBehavior(taskRemovedBehavior) registerNavigationListeners() navigationReadyListener?.onNavigationReady(true) - // Mark initialization as complete - SharedNavigatorHolder.setInitializationInProgress(false) - // Notify all queued callbacks val queuedCallbacks = SharedNavigatorHolder.getAndClearInitializationCallbacks() for (queuedCallback in queuedCallbacks) { @@ -243,7 +257,7 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : } override fun onError(@NavigationApi.ErrorCode errorCode: Int) { - SharedNavigatorHolder.setInitializationInProgress(false) + SharedNavigatorHolder.setInitializationState(GoogleNavigatorInitializationState.NOT_INITIALIZED) val error = convertNavigatorErrorToFlutterError(errorCode) From 06d3cdec8418344667b66debf292f95be992e96d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20V=C3=A4limaa?= Date: Fri, 31 Oct 2025 12:17:13 +0800 Subject: [PATCH 04/10] Refactor SharedNavigatorHolder to GoogleMapsNavigatorHolder in its own file --- .../navigation/AndroidAutoBaseScreen.kt | 2 +- .../GoogleMapsNavigationInspectorHandler.kt | 2 +- .../GoogleMapsNavigationSessionManager.kt | 81 +++---------------- ...ogleMapsNavigationSessionMessageHandler.kt | 2 +- .../navigation/GoogleMapsNavigationView.kt | 2 +- .../navigation/GoogleMapsNavigatorHolder.kt | 61 ++++++++++++++ 6 files changed, 77 insertions(+), 73 deletions(-) create mode 100644 android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigatorHolder.kt diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt index ac5335fb..b1d13daf 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt @@ -55,7 +55,7 @@ open class AndroidAutoBaseScreen(carContext: CarContext) : private fun initializeNavigationListener() { GoogleMapsNavigationSessionManager.navigationReadyListener = this - mIsNavigationReady = SharedNavigatorHolder.getNavigator() != null + mIsNavigationReady = GoogleMapsNavigatorHolder.getNavigator() != null } private fun initializeSurfaceCallback() { diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt index dd4a020b..7bb4a519 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt @@ -20,7 +20,7 @@ class GoogleMapsNavigationInspectorHandler( override fun isViewAttachedToSession(viewId: Long): Boolean { /// Is session exists, it's automatically attached to any existing view. if (viewRegistry.getNavigationView(viewId.toInt()) != null) { - return SharedNavigatorHolder.getNavigator() != null + return GoogleMapsNavigatorHolder.getNavigator() != null } return false } 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 50476096..3a48daf0 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 @@ -51,63 +51,6 @@ interface NavigationReadyListener { fun onNavigationReady(ready: Boolean) } -/** - * Singleton holder for the shared Navigator instance. - * Multiple GoogleMapsNavigationSessionManager instances share the same Navigator. - */ -enum class GoogleNavigatorInitializationState { - NOT_INITIALIZED, - INITIALIZING, - INITIALIZED, -} - -object SharedNavigatorHolder { - @Volatile - private var navigator: Navigator? = null - var initializationState = GoogleNavigatorInitializationState.NOT_INITIALIZED - private val initializationCallbacks = mutableListOf() - - @Synchronized - fun getNavigator(): Navigator? = navigator - - @Synchronized - fun setNavigator(nav: Navigator?) { - navigator = nav - initializationState = if (nav != null) { - GoogleNavigatorInitializationState.INITIALIZED - } else { - GoogleNavigatorInitializationState.NOT_INITIALIZED - } - } - - @Synchronized - fun getInitializationState(): GoogleNavigatorInitializationState = initializationState - - @Synchronized - fun setInitializationState(state: GoogleNavigatorInitializationState) { - initializationState = state - } - - @Synchronized - fun addInitializationCallback(callback: NavigatorListener) { - initializationCallbacks.add(callback) - } - - @Synchronized - fun getAndClearInitializationCallbacks(): List { - val callbacks = initializationCallbacks.toList() - initializationCallbacks.clear() - return callbacks - } - - @Synchronized - fun reset() { - navigator = null - initializationState = GoogleNavigatorInitializationState.NOT_INITIALIZED - initializationCallbacks.clear() - } -} - /** This class handles creation of navigation session and other navigation related tasks. */ class GoogleMapsNavigationSessionManager constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : @@ -168,7 +111,7 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : @Throws(FlutterError::class) fun getNavigator(): Navigator { - val nav = SharedNavigatorHolder.getNavigator() + val nav = GoogleMapsNavigatorHolder.getNavigator() if (nav != null) { return nav } else { @@ -185,7 +128,7 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : behavior: TaskRemovedBehaviorDto, callback: (Result) -> Unit, ) { - val currentState = SharedNavigatorHolder.getInitializationState() + val currentState = GoogleMapsNavigatorHolder.getInitializationState() if (currentState == GoogleNavigatorInitializationState.INITIALIZED) { // Navigator is already initialized, just re-register listeners. @@ -209,7 +152,7 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : callback(Result.failure(convertNavigatorErrorToFlutterError(errorCode))) } } - SharedNavigatorHolder.addInitializationCallback(queuedListener) + GoogleMapsNavigatorHolder.addInitializationCallback(queuedListener) return } @@ -233,22 +176,22 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : NavigationApi.setAbnormalTerminationReportingEnabled(abnormalTerminationReportingEnabled) // Mark initialization as in progress - SharedNavigatorHolder.setInitializationState(GoogleNavigatorInitializationState.INITIALIZING) + GoogleMapsNavigatorHolder.setInitializationState(GoogleNavigatorInitializationState.INITIALIZING) val listener = object : NavigatorListener { override fun onNavigatorReady(newNavigator: Navigator) { - if (SharedNavigatorHolder.initializationState != GoogleNavigatorInitializationState.INITIALIZING) { - SharedNavigatorHolder.setNavigator(null); + if (GoogleMapsNavigatorHolder.getInitializationState() != GoogleNavigatorInitializationState.INITIALIZING) { + GoogleMapsNavigatorHolder.setNavigator(null); return } - SharedNavigatorHolder.setNavigator(newNavigator) + GoogleMapsNavigatorHolder.setNavigator(newNavigator) newNavigator.setTaskRemovedBehavior(taskRemovedBehavior) registerNavigationListeners() navigationReadyListener?.onNavigationReady(true) // Notify all queued callbacks - val queuedCallbacks = SharedNavigatorHolder.getAndClearInitializationCallbacks() + val queuedCallbacks = GoogleMapsNavigatorHolder.getAndClearInitializationCallbacks() for (queuedCallback in queuedCallbacks) { queuedCallback.onNavigatorReady(newNavigator) } @@ -257,12 +200,12 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : } override fun onError(@NavigationApi.ErrorCode errorCode: Int) { - SharedNavigatorHolder.setInitializationState(GoogleNavigatorInitializationState.NOT_INITIALIZED) + GoogleMapsNavigatorHolder.setInitializationState(GoogleNavigatorInitializationState.NOT_INITIALIZED) val error = convertNavigatorErrorToFlutterError(errorCode) // Notify all queued callbacks about the error - val queuedCallbacks = SharedNavigatorHolder.getAndClearInitializationCallbacks() + val queuedCallbacks = GoogleMapsNavigatorHolder.getAndClearInitializationCallbacks() for (queuedCallback in queuedCallbacks) { queuedCallback.onError(errorCode) } @@ -338,12 +281,12 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : // As unregisterListeners() is removing all listeners, we need to re-register them when // navigator is re-initialized. This is done in createNavigationSession() method. - SharedNavigatorHolder.setNavigator(null) + GoogleMapsNavigatorHolder.setNavigator(null) navigationReadyListener?.onNavigationReady(false) } private fun unregisterListeners() { - val navigator = SharedNavigatorHolder.getNavigator() + val navigator = GoogleMapsNavigatorHolder.getNavigator() if (navigator != null) { if (remainingTimeOrDistanceChangedListener != null) { navigator.removeRemainingTimeOrDistanceChangedListener( 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 8343d565..bab0798a 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 @@ -33,7 +33,7 @@ class GoogleMapsNavigationSessionMessageHandler( } override fun isInitialized(): Boolean { - return SharedNavigatorHolder.getNavigator() != null + return GoogleMapsNavigatorHolder.getNavigator() != null } override fun cleanup() { diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt index d999e0c7..c86b651a 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt @@ -65,7 +65,7 @@ internal constructor( // Initialize navigation view with given navigation view options var navigationViewEnabled = false if ( - navigationOptions?.navigationUiEnabledPreference == NavigationUIEnabledPreference.AUTOMATIC && SharedNavigatorHolder.getNavigator() != null + navigationOptions?.navigationUiEnabledPreference == NavigationUIEnabledPreference.AUTOMATIC && GoogleMapsNavigatorHolder.getNavigator() != null ) { navigationViewEnabled = true } 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 new file mode 100644 index 00000000..80ebec93 --- /dev/null +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigatorHolder.kt @@ -0,0 +1,61 @@ +package com.google.maps.flutter.navigation + +import com.google.android.libraries.navigation.NavigationApi +import com.google.android.libraries.navigation.Navigator + +/** + * Singleton holder for the shared Navigator instance. + * Multiple GoogleMapsNavigationSessionManager instances share the same Navigator. + */ +enum class GoogleNavigatorInitializationState { + NOT_INITIALIZED, + INITIALIZING, + INITIALIZED, +} + +object GoogleMapsNavigatorHolder { + @Volatile + private var navigator: Navigator? = null + private var initializationState = GoogleNavigatorInitializationState.NOT_INITIALIZED + private val initializationCallbacks = mutableListOf() + + @Synchronized + fun getNavigator(): Navigator? = navigator + + @Synchronized + fun setNavigator(nav: Navigator?) { + navigator = nav + initializationState = if (nav != null) { + GoogleNavigatorInitializationState.INITIALIZED + } else { + GoogleNavigatorInitializationState.NOT_INITIALIZED + } + } + + @Synchronized + fun getInitializationState(): GoogleNavigatorInitializationState = initializationState + + @Synchronized + fun setInitializationState(state: GoogleNavigatorInitializationState) { + initializationState = state + } + + @Synchronized + fun addInitializationCallback(callback: NavigationApi.NavigatorListener) { + initializationCallbacks.add(callback) + } + + @Synchronized + fun getAndClearInitializationCallbacks(): List { + val callbacks = initializationCallbacks.toList() + initializationCallbacks.clear() + return callbacks + } + + @Synchronized + fun reset() { + navigator = null + initializationState = GoogleNavigatorInitializationState.NOT_INITIALIZED + initializationCallbacks.clear() + } +} \ No newline at end of file From 25a6731561731a8acc3249bab2fafa192e90e8f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20V=C3=A4limaa?= Date: Fri, 31 Oct 2025 16:59:06 +0800 Subject: [PATCH 05/10] small refactoring --- .../google/maps/flutter/navigation/AndroidAutoBaseScreen.kt | 2 +- .../flutter/navigation/GoogleMapsNavigationInspectorHandler.kt | 2 +- .../navigation/GoogleMapsNavigationSessionMessageHandler.kt | 2 +- .../google/maps/flutter/navigation/GoogleMapsNavigationView.kt | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt index b1d13daf..0e54fd48 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt @@ -55,7 +55,7 @@ open class AndroidAutoBaseScreen(carContext: CarContext) : private fun initializeNavigationListener() { GoogleMapsNavigationSessionManager.navigationReadyListener = this - mIsNavigationReady = GoogleMapsNavigatorHolder.getNavigator() != null + mIsNavigationReady = GoogleMapsNavigatorHolder.getInitializationState() == GoogleNavigatorInitializationState.INITIALIZED } private fun initializeSurfaceCallback() { diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt index 7bb4a519..f9b5e19f 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt @@ -20,7 +20,7 @@ class GoogleMapsNavigationInspectorHandler( override fun isViewAttachedToSession(viewId: Long): Boolean { /// Is session exists, it's automatically attached to any existing view. if (viewRegistry.getNavigationView(viewId.toInt()) != null) { - return GoogleMapsNavigatorHolder.getNavigator() != null + return GoogleMapsNavigatorHolder.getInitializationState() == GoogleNavigatorInitializationState.INITIALIZED } return false } 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 bab0798a..75946822 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 @@ -33,7 +33,7 @@ class GoogleMapsNavigationSessionMessageHandler( } override fun isInitialized(): Boolean { - return GoogleMapsNavigatorHolder.getNavigator() != null + return GoogleMapsNavigatorHolder.getInitializationState() == GoogleNavigatorInitializationState.INITIALIZED } override fun cleanup() { diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt index c86b651a..4e045955 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt @@ -65,7 +65,8 @@ internal constructor( // Initialize navigation view with given navigation view options var navigationViewEnabled = false if ( - navigationOptions?.navigationUiEnabledPreference == NavigationUIEnabledPreference.AUTOMATIC && GoogleMapsNavigatorHolder.getNavigator() != null + navigationOptions?.navigationUiEnabledPreference == NavigationUIEnabledPreference.AUTOMATIC + && GoogleMapsNavigatorHolder.getInitializationState() == GoogleNavigatorInitializationState.INITIALIZED ) { navigationViewEnabled = true } From 5fb5812779ca8e82048b0499b37b9c0621bb1b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20V=C3=A4limaa?= Date: Thu, 6 Nov 2025 11:09:42 +0800 Subject: [PATCH 06/10] test: fix navigation integrations tests --- example/integration_test/t03_navigation_test.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/example/integration_test/t03_navigation_test.dart b/example/integration_test/t03_navigation_test.dart index e7569156..5b1e58e7 100644 --- a/example/integration_test/t03_navigation_test.dart +++ b/example/integration_test/t03_navigation_test.dart @@ -418,12 +418,9 @@ void main() { $.log('Starting loop with simulator$loopIteration.'); loopIteration += 1; - /// Initialize navigation if iOS. - /// On iOS .cleanup() destroys the initialization. - if (Platform.isIOS) { - await GoogleMapsNavigator.initializeNavigationSession(); - await $.pumpAndSettle(); - } + /// Initialize navigation as .cleanup() destroys the initialization. + await GoogleMapsNavigator.initializeNavigationSession(); + await $.pumpAndSettle(); /// Simulate location and test it. await setSimulatedUserLocationWithCheck( From 36b109e906d0c2de1e751002508a1e5f4c2b4f01 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Wed, 12 Nov 2025 12:27:14 +0200 Subject: [PATCH 07/10] chore: format kotlin code --- .../navigation/AndroidAutoBaseScreen.kt | 4 +- .../GoogleMapsNavigationInspectorHandler.kt | 8 +- .../navigation/GoogleMapsNavigationPlugin.kt | 9 +- .../GoogleMapsNavigationSessionManager.kt | 62 ++++++------ ...ogleMapsNavigationSessionMessageHandler.kt | 32 +++--- .../navigation/GoogleMapsNavigationView.kt | 7 +- .../navigation/GoogleMapsNavigatorHolder.kt | 99 +++++++++---------- .../navigation/GoogleMapsViewFactory.kt | 4 +- example/ios/Flutter/AppFrameworkInfo.plist | 2 +- example/ios/Runner.xcodeproj/project.pbxproj | 22 +++++ .../xcshareddata/swiftpm/Package.resolved | 23 +++++ .../xcshareddata/xcschemes/Runner.xcscheme | 18 ++++ .../xcshareddata/swiftpm/Package.resolved | 23 +++++ 13 files changed, 203 insertions(+), 110 deletions(-) create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt index 0e54fd48..3892c324 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt @@ -55,7 +55,9 @@ open class AndroidAutoBaseScreen(carContext: CarContext) : private fun initializeNavigationListener() { GoogleMapsNavigationSessionManager.navigationReadyListener = this - mIsNavigationReady = GoogleMapsNavigatorHolder.getInitializationState() == GoogleNavigatorInitializationState.INITIALIZED + mIsNavigationReady = + GoogleMapsNavigatorHolder.getInitializationState() == + GoogleNavigatorInitializationState.INITIALIZED } private fun initializeSurfaceCallback() { diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt index f9b5e19f..974c98c2 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationInspectorHandler.kt @@ -14,13 +14,13 @@ package com.google.maps.flutter.navigation -class GoogleMapsNavigationInspectorHandler( - private val viewRegistry: GoogleMapsViewRegistry -) : NavigationInspector { +class GoogleMapsNavigationInspectorHandler(private val viewRegistry: GoogleMapsViewRegistry) : + NavigationInspector { override fun isViewAttachedToSession(viewId: Long): Boolean { /// Is session exists, it's automatically attached to any existing view. if (viewRegistry.getNavigationView(viewId.toInt()) != null) { - return GoogleMapsNavigatorHolder.getInitializationState() == GoogleNavigatorInitializationState.INITIALIZED + return GoogleMapsNavigatorHolder.getInitializationState() == + GoogleNavigatorInitializationState.INITIALIZED } return false } 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 4a83833c..4cacb216 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 @@ -40,14 +40,15 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { private var viewMessageHandler: GoogleMapsViewMessageHandler? = null private var imageRegistryMessageHandler: GoogleMapsImageRegistryMessageHandler? = null private var autoViewMessageHandler: GoogleMapsAutoViewMessageHandler? = null - + // Instance-level session manager instead of singleton internal var sessionManager: GoogleMapsNavigationSessionManager? = null private var lifecycle: Lifecycle? = null override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - // Store first instance of the plugin so that Android Auto will get access to the correct object instances. + // Store first instance of the plugin so that Android Auto will get access to the correct object + // instances. if (instance == null) { instance = this } @@ -76,11 +77,11 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { viewEventApi = ViewEventApi(binding.binaryMessenger) val factory = GoogleMapsViewFactory(viewRegistry!!, viewEventApi!!, imageRegistry!!) binding.platformViewRegistry.registerViewFactory("google_navigation_flutter", factory) - + // Setup navigation session message handler with this instance's session manager val sessionMessageHandler = GoogleMapsNavigationSessionMessageHandler(sessionManager!!) NavigationSessionApi.setUp(binding.binaryMessenger, sessionMessageHandler) - + val inspectorHandler = GoogleMapsNavigationInspectorHandler(viewRegistry!!) NavigationInspector.setUp(binding.binaryMessenger, inspectorHandler) } 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 3a48daf0..08b7f896 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 @@ -44,7 +44,6 @@ import com.google.android.libraries.navigation.TermsAndConditionsUIParams import com.google.android.libraries.navigation.TimeAndDistance import com.google.android.libraries.navigation.Waypoint import com.google.maps.flutter.navigation.Convert.convertTravelModeFromDto -import io.flutter.plugin.common.BinaryMessenger import java.lang.ref.WeakReference interface NavigationReadyListener { @@ -129,7 +128,7 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : callback: (Result) -> Unit, ) { val currentState = GoogleMapsNavigatorHolder.getInitializationState() - + if (currentState == GoogleNavigatorInitializationState.INITIALIZED) { // Navigator is already initialized, just re-register listeners. registerNavigationListeners() @@ -137,25 +136,26 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : callback(Result.success(Unit)) return } - + // Check if initialization is already in progress by another instance if (currentState == GoogleNavigatorInitializationState.INITIALIZING) { // Add this callback to the queue to be called when initialization completes - val queuedListener = object : NavigatorListener { - override fun onNavigatorReady(newNavigator: Navigator) { - registerNavigationListeners() - navigationReadyListener?.onNavigationReady(true) - callback(Result.success(Unit)) - } + val queuedListener = + object : NavigatorListener { + override fun onNavigatorReady(newNavigator: Navigator) { + registerNavigationListeners() + navigationReadyListener?.onNavigationReady(true) + callback(Result.success(Unit)) + } - override fun onError(@NavigationApi.ErrorCode errorCode: Int) { - callback(Result.failure(convertNavigatorErrorToFlutterError(errorCode))) + override fun onError(@NavigationApi.ErrorCode errorCode: Int) { + callback(Result.failure(convertNavigatorErrorToFlutterError(errorCode))) + } } - } GoogleMapsNavigatorHolder.addInitializationCallback(queuedListener) return } - + taskRemovedBehavior = Convert.taskRemovedBehaviorDtoToTaskRemovedBehavior(behavior) // Align API behavior with iOS: @@ -176,48 +176,57 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : NavigationApi.setAbnormalTerminationReportingEnabled(abnormalTerminationReportingEnabled) // Mark initialization as in progress - GoogleMapsNavigatorHolder.setInitializationState(GoogleNavigatorInitializationState.INITIALIZING) + GoogleMapsNavigatorHolder.setInitializationState( + GoogleNavigatorInitializationState.INITIALIZING + ) val listener = object : NavigatorListener { override fun onNavigatorReady(newNavigator: Navigator) { - if (GoogleMapsNavigatorHolder.getInitializationState() != GoogleNavigatorInitializationState.INITIALIZING) { - GoogleMapsNavigatorHolder.setNavigator(null); + if ( + GoogleMapsNavigatorHolder.getInitializationState() != + GoogleNavigatorInitializationState.INITIALIZING + ) { + GoogleMapsNavigatorHolder.setNavigator(null) return } GoogleMapsNavigatorHolder.setNavigator(newNavigator) newNavigator.setTaskRemovedBehavior(taskRemovedBehavior) registerNavigationListeners() navigationReadyListener?.onNavigationReady(true) - + // Notify all queued callbacks val queuedCallbacks = GoogleMapsNavigatorHolder.getAndClearInitializationCallbacks() for (queuedCallback in queuedCallbacks) { queuedCallback.onNavigatorReady(newNavigator) } - + callback(Result.success(Unit)) } override fun onError(@NavigationApi.ErrorCode errorCode: Int) { - GoogleMapsNavigatorHolder.setInitializationState(GoogleNavigatorInitializationState.NOT_INITIALIZED) - + GoogleMapsNavigatorHolder.setInitializationState( + GoogleNavigatorInitializationState.NOT_INITIALIZED + ) + val error = convertNavigatorErrorToFlutterError(errorCode) - + // Notify all queued callbacks about the error val queuedCallbacks = GoogleMapsNavigatorHolder.getAndClearInitializationCallbacks() for (queuedCallback in queuedCallbacks) { queuedCallback.onError(errorCode) } - + callback(Result.failure(error)) } } NavigationApi.getNavigator(getActivity(), listener) } - - private fun convertNavigatorErrorToFlutterError(@NavigationApi.ErrorCode errorCode: Int): FlutterError { + + private fun convertNavigatorErrorToFlutterError( + @NavigationApi.ErrorCode errorCode: Int + ): FlutterError { // Keep in sync with GoogleMapsNavigationSessionManager.swift return when (errorCode) { NavigationApi.ErrorCode.NOT_AUTHORIZED -> { @@ -245,10 +254,7 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : ) } else -> { - FlutterError( - "unknownError", - "The session initialization failed with an unknown error.", - ) + FlutterError("unknownError", "The session initialization failed with an unknown error.") } } } 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 75946822..ef233b07 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 @@ -33,7 +33,8 @@ class GoogleMapsNavigationSessionMessageHandler( } override fun isInitialized(): Boolean { - return GoogleMapsNavigatorHolder.getInitializationState() == GoogleNavigatorInitializationState.INITIALIZED + return GoogleMapsNavigatorHolder.getInitializationState() == + GoogleNavigatorInitializationState.INITIALIZED } override fun cleanup() { @@ -46,13 +47,12 @@ class GoogleMapsNavigationSessionMessageHandler( shouldOnlyShowDriverAwarenessDisclaimer: Boolean, callback: (Result) -> Unit, ) { - sessionManager - .showTermsAndConditionsDialog( - title, - companyName, - shouldOnlyShowDriverAwarenessDisclaimer, - callback, - ) + sessionManager.showTermsAndConditionsDialog( + title, + companyName, + shouldOnlyShowDriverAwarenessDisclaimer, + callback, + ) } override fun areTermsAccepted(): Boolean { @@ -168,10 +168,9 @@ class GoogleMapsNavigationSessionMessageHandler( } override fun simulateLocationsAlongExistingRouteWithOptions(options: SimulationOptionsDto) { - sessionManager - .simulateLocationsAlongExistingRouteWithOptions( - SimulationOptions().speedMultiplier(options.speedMultiplier.toFloat()) - ) + sessionManager.simulateLocationsAlongExistingRouteWithOptions( + SimulationOptions().speedMultiplier(options.speedMultiplier.toFloat()) + ) } override fun simulateLocationsAlongNewRoute( @@ -267,10 +266,9 @@ class GoogleMapsNavigationSessionMessageHandler( remainingTimeThresholdSeconds: Long, remainingDistanceThresholdMeters: Long, ) { - sessionManager - .registerRemainingTimeOrDistanceChangedListener( - remainingTimeThresholdSeconds, - remainingDistanceThresholdMeters, - ) + sessionManager.registerRemainingTimeOrDistanceChangedListener( + remainingTimeThresholdSeconds, + remainingDistanceThresholdMeters, + ) } } diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt index 4e045955..d4ee0ccf 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt @@ -32,7 +32,7 @@ internal constructor( viewId: Int, private val viewRegistry: GoogleMapsViewRegistry, viewEventApi: ViewEventApi, - private val imageRegistry: ImageRegistry + private val imageRegistry: ImageRegistry, ) : PlatformView, GoogleMapsBaseMapView(viewId, mapOptions, viewEventApi, imageRegistry) { private val _navigationView: NavigationView = NavigationView(context, mapOptions.googleMapOptions) @@ -65,8 +65,9 @@ internal constructor( // Initialize navigation view with given navigation view options var navigationViewEnabled = false if ( - navigationOptions?.navigationUiEnabledPreference == NavigationUIEnabledPreference.AUTOMATIC - && GoogleMapsNavigatorHolder.getInitializationState() == GoogleNavigatorInitializationState.INITIALIZED + navigationOptions?.navigationUiEnabledPreference == NavigationUIEnabledPreference.AUTOMATIC && + GoogleMapsNavigatorHolder.getInitializationState() == + GoogleNavigatorInitializationState.INITIALIZED ) { navigationViewEnabled = true } 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 80ebec93..b851758f 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 @@ -4,58 +4,57 @@ import com.google.android.libraries.navigation.NavigationApi import com.google.android.libraries.navigation.Navigator /** - * Singleton holder for the shared Navigator instance. - * Multiple GoogleMapsNavigationSessionManager instances share the same Navigator. + * Singleton holder for the shared Navigator instance. Multiple GoogleMapsNavigationSessionManager + * instances share the same Navigator. */ enum class GoogleNavigatorInitializationState { - NOT_INITIALIZED, - INITIALIZING, - INITIALIZED, + NOT_INITIALIZED, + INITIALIZING, + INITIALIZED, } object GoogleMapsNavigatorHolder { - @Volatile - private var navigator: Navigator? = null - private var initializationState = GoogleNavigatorInitializationState.NOT_INITIALIZED - private val initializationCallbacks = mutableListOf() - - @Synchronized - fun getNavigator(): Navigator? = navigator - - @Synchronized - fun setNavigator(nav: Navigator?) { - navigator = nav - initializationState = if (nav != null) { - GoogleNavigatorInitializationState.INITIALIZED - } else { - GoogleNavigatorInitializationState.NOT_INITIALIZED - } - } - - @Synchronized - fun getInitializationState(): GoogleNavigatorInitializationState = initializationState - - @Synchronized - fun setInitializationState(state: GoogleNavigatorInitializationState) { - initializationState = state - } - - @Synchronized - fun addInitializationCallback(callback: NavigationApi.NavigatorListener) { - initializationCallbacks.add(callback) - } - - @Synchronized - fun getAndClearInitializationCallbacks(): List { - val callbacks = initializationCallbacks.toList() - initializationCallbacks.clear() - return callbacks - } - - @Synchronized - fun reset() { - navigator = null - initializationState = GoogleNavigatorInitializationState.NOT_INITIALIZED - initializationCallbacks.clear() - } -} \ No newline at end of file + @Volatile private var navigator: Navigator? = null + private var initializationState = GoogleNavigatorInitializationState.NOT_INITIALIZED + private val initializationCallbacks = mutableListOf() + + @Synchronized fun getNavigator(): Navigator? = navigator + + @Synchronized + fun setNavigator(nav: Navigator?) { + navigator = nav + initializationState = + if (nav != null) { + GoogleNavigatorInitializationState.INITIALIZED + } else { + GoogleNavigatorInitializationState.NOT_INITIALIZED + } + } + + @Synchronized + fun getInitializationState(): GoogleNavigatorInitializationState = initializationState + + @Synchronized + fun setInitializationState(state: GoogleNavigatorInitializationState) { + initializationState = state + } + + @Synchronized + fun addInitializationCallback(callback: NavigationApi.NavigatorListener) { + initializationCallbacks.add(callback) + } + + @Synchronized + fun getAndClearInitializationCallbacks(): List { + val callbacks = initializationCallbacks.toList() + initializationCallbacks.clear() + return callbacks + } + + @Synchronized + fun reset() { + navigator = null + initializationState = GoogleNavigatorInitializationState.NOT_INITIALIZED + initializationCallbacks.clear() + } +} diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsViewFactory.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsViewFactory.kt index e6e9f6a1..9e3194bf 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsViewFactory.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsViewFactory.kt @@ -24,7 +24,7 @@ import java.util.Objects class GoogleMapsViewFactory( private val viewRegistry: GoogleMapsViewRegistry, private val viewEventApi: ViewEventApi, - private val imageRegistry: ImageRegistry + private val imageRegistry: ImageRegistry, ) : PlatformViewFactory(ViewCreationApi.codec) { override fun create(context: Context, viewId: Int, args: Any?): PlatformView { val params = Objects.requireNonNull(args as ViewCreationOptionsDto) @@ -39,7 +39,7 @@ class GoogleMapsViewFactory( viewId, viewRegistry, viewEventApi, - imageRegistry + imageRegistry, ) } else { return GoogleMapView(context, mapOptions, viewId, viewEventApi, viewRegistry, imageRegistry) 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 @@ + + + + + + + + + + Date: Fri, 14 Nov 2025 12:51:38 +0200 Subject: [PATCH 08/10] feat: add improve navigation session lifecycle control (#524) --- .../navigation/GoogleMapsNavigationPlugin.kt | 6 +- .../GoogleMapsNavigationSessionManager.kt | 118 +++++++----------- ...ogleMapsNavigationSessionMessageHandler.kt | 4 +- .../navigation/GoogleMapsNavigatorHolder.kt | 97 ++++++++++++++ .../maps/flutter/navigation/messages.g.kt | 8 +- .../integration_test/t09_isolates_test.dart | 100 +++++++++++++++ .../GoogleMapsNavigationSessionManager.swift | 16 ++- ...eMapsNavigationSessionMessageHandler.swift | 4 +- .../messages.g.swift | 8 +- lib/src/method_channel/messages.g.dart | 6 +- lib/src/method_channel/session_api.dart | 12 +- .../google_navigation_flutter_navigator.dart | 29 +++-- pigeons/messages.dart | 2 +- .../google_navigation_flutter_test.mocks.dart | 4 +- test/messages_test.g.dart | 14 ++- 15 files changed, 315 insertions(+), 113 deletions(-) create mode 100644 example/integration_test/t09_isolates_test.dart 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); From f146116878f8f376ddd547596c73ba7d2eb14f69 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Mon, 17 Nov 2025 12:11:41 +0200 Subject: [PATCH 09/10] ci: limit emulator memory usage --- .github/workflows/test-and-build.yaml | 11 +++++++++++ .../flutter/navigation/GoogleMapsNavigationPlugin.kt | 2 -- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-and-build.yaml b/.github/workflows/test-and-build.yaml index 7b77f535..2fc3627c 100644 --- a/.github/workflows/test-and-build.yaml +++ b/.github/workflows/test-and-build.yaml @@ -276,6 +276,17 @@ jobs: run: flutter pub global activate patrol_cli ${{ env.patrol_cli_version }} - name: Run flutter pub get run: flutter pub get + - name: Clean up runner disk space (if needed) + run: | + echo "Running cleanup..." + + # Remove large, unneeded packages + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/share/boost + + echo "Cleanup complete. Free space after cleanup:" + df -h - name: Create and start emulator run: | echo "Installing system image" 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 1d1e43ce..69bbba02 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 @@ -41,8 +41,6 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { private var viewMessageHandler: GoogleMapsViewMessageHandler? = null private var imageRegistryMessageHandler: GoogleMapsImageRegistryMessageHandler? = null private var autoViewMessageHandler: GoogleMapsAutoViewMessageHandler? = null - - // Instance-level session manager instead of singleton internal var sessionManager: GoogleMapsNavigationSessionManager? = null private var lifecycle: Lifecycle? = null From a3e597b6142a2447ac9105df53c9aeccbbf9ac3c Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Tue, 18 Nov 2025 13:20:20 +0200 Subject: [PATCH 10/10] fix: plugin instance handling on Android --- .../navigation/GoogleMapsNavigationPlugin.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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 69bbba02..0f90780a 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 @@ -26,10 +26,11 @@ import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter /** GoogleMapsNavigationPlugin */ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { companion object { - private var instance: GoogleMapsNavigationPlugin? = null + private val instances = mutableListOf() + /** Returns the first instance, which should always be the main Flutter engine. */ fun getInstance(): GoogleMapsNavigationPlugin? { - return instance + return instances.firstOrNull() } } @@ -46,11 +47,7 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { private var lifecycle: Lifecycle? = null override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - // Store first instance of the plugin so that Android Auto will get access to the correct object - // instances. - if (instance == null) { - instance = this - } + synchronized(instances) { instances.add(this) } // Init view registry and its method channel handlers viewRegistry = GoogleMapsViewRegistry() @@ -104,7 +101,8 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { imageRegistry = null autoViewMessageHandler = null autoViewEventApi = null - instance = null + + synchronized(instances) { instances.remove(this) } } private fun attachActivity(binding: ActivityPluginBinding) {