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/AndroidAutoBaseScreen.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt index 7446158b..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 @@ -56,12 +56,8 @@ open class AndroidAutoBaseScreen(carContext: CarContext) : private fun initializeNavigationListener() { GoogleMapsNavigationSessionManager.navigationReadyListener = this mIsNavigationReady = - try { - GoogleMapsNavigationSessionManager.getInstance().isInitialized() - } catch (exception: RuntimeException) { - // If GoogleMapsNavigationSessionManager is not initialized navigation is not ready. - false - } + 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 6a67bde5..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 @@ -16,14 +16,11 @@ package com.google.maps.flutter.navigation class GoogleMapsNavigationInspectorHandler(private val viewRegistry: GoogleMapsViewRegistry) : NavigationInspector { - private fun manager(): GoogleMapsNavigationSessionManager { - return GoogleMapsNavigationSessionManager.getInstance() - } - 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 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 78866d8c..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 @@ -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 @@ -25,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() } } @@ -40,11 +42,12 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { private var viewMessageHandler: GoogleMapsViewMessageHandler? = null private var imageRegistryMessageHandler: GoogleMapsImageRegistryMessageHandler? = null private var autoViewMessageHandler: GoogleMapsAutoViewMessageHandler? = null + internal var sessionManager: GoogleMapsNavigationSessionManager? = null private var lifecycle: Lifecycle? = null override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - instance = this + synchronized(instances) { instances.add(this) } // Init view registry and its method channel handlers viewRegistry = GoogleMapsViewRegistry() @@ -57,18 +60,25 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { imageRegistryMessageHandler = GoogleMapsImageRegistryMessageHandler(imageRegistry!!) ImageRegistryApi.setUp(binding.binaryMessenger, imageRegistryMessageHandler) + // Setup auto map view method channel handlers + autoViewMessageHandler = GoogleMapsAutoViewMessageHandler(viewRegistry!!) + AutoMapViewApi.setUp(binding.binaryMessenger, autoViewMessageHandler) + autoViewEventApi = AutoViewEventApi(binding.binaryMessenger) + + // Setup navigation session manager + val app = binding.applicationContext as Application + val navigationSessionEventApi = NavigationSessionEventApi(binding.binaryMessenger) + sessionManager = GoogleMapsNavigationSessionManager(navigationSessionEventApi, app) + // 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) - autoViewEventApi = AutoViewEventApi(binding.binaryMessenger) + // Setup navigation session message handler with this instance's session manager + val sessionMessageHandler = GoogleMapsNavigationSessionMessageHandler(sessionManager!!) + NavigationSessionApi.setUp(binding.binaryMessenger, sessionMessageHandler) - // Setup navigation session manager and its method channel handlers - GoogleMapsNavigationSessionManager.createInstance(binding.binaryMessenger) val inspectorHandler = GoogleMapsNavigationInspectorHandler(viewRegistry!!) NavigationInspector.setUp(binding.binaryMessenger, inspectorHandler) } @@ -80,10 +90,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 @@ -91,25 +101,26 @@ class GoogleMapsNavigationPlugin : FlutterPlugin, ActivityAware { imageRegistry = null autoViewMessageHandler = null autoViewEventApi = null - instance = null + + synchronized(instances) { instances.remove(this) } } private fun attachActivity(binding: ActivityPluginBinding) { 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..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 @@ -44,7 +42,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 { @@ -53,61 +50,14 @@ interface NavigationReadyListener { /** This class handles creation of navigation session and other navigation related tasks. */ class GoogleMapsNavigationSessionManager -private constructor(private val navigationSessionEventApi: NavigationSessionEventApi) : - DefaultLifecycleObserver { +constructor( + private val navigationSessionEventApi: NavigationSessionEventApi, + private val application: Application, +) : 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 private var reroutingListener: Navigator.ReroutingListener? = null @@ -121,7 +71,7 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven 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 @@ -160,8 +110,9 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven @Throws(FlutterError::class) fun getNavigator(): Navigator { - if (navigator != null) { - return navigator!! + val nav = GoogleMapsNavigatorHolder.getNavigator() + if (nav != null) { + return nav } else { throw FlutterError( "sessionNotInitialized", @@ -170,26 +121,41 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven } } - // Expose the navigator to the google_maps_driver side. - // DriverApi initialization requires navigator. - fun getNavigatorWithoutError(): Navigator? { - return navigator - } - /** Creates Navigator instance. */ fun createNavigationSession( abnormalTerminationReportingEnabled: Boolean, behavior: TaskRemovedBehaviorDto, callback: (Result) -> Unit, ) { - if (navigator != null) { + val currentState = GoogleMapsNavigatorHolder.getInitializationState() + + if (currentState == GoogleNavigatorInitializationState.INITIALIZED) { // Navigator is already initialized, just re-register listeners. registerNavigationListeners() - isNavigationSessionInitialized = true navigationReadyListener?.onNavigationReady(true) 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)) + } + + 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: @@ -209,65 +175,88 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven // Enable or disable abnormal termination reporting. NavigationApi.setAbnormalTerminationReportingEnabled(abnormalTerminationReportingEnabled) + // Mark initialization as in progress + GoogleMapsNavigatorHolder.setInitializationState( + GoogleNavigatorInitializationState.INITIALIZING + ) + val listener = object : NavigatorListener { override fun onNavigatorReady(newNavigator: Navigator) { - navigator = newNavigator - navigator?.setTaskRemovedBehavior(taskRemovedBehavior) + if ( + GoogleMapsNavigatorHolder.getInitializationState() != + GoogleNavigatorInitializationState.INITIALIZING + ) { + GoogleMapsNavigatorHolder.setNavigator(null) + return + } + GoogleMapsNavigatorHolder.setNavigator(newNavigator) + newNavigator.setTaskRemovedBehavior(taskRemovedBehavior) registerNavigationListeners() - isNavigationSessionInitialized = true 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) { - // 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.", - ) - ) - ) - } + 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) + NavigationApi.getNavigator(application, 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) @@ -275,36 +264,31 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven 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. - isNavigationSessionInitialized = false - 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() { - if (isInitialized()) { - val navigator = getNavigator() + internal fun unregisterListeners() { + val navigator = GoogleMapsNavigatorHolder.getNavigator() + if (navigator != null) { if (remainingTimeOrDistanceChangedListener != null) { navigator.removeRemainingTimeOrDistanceChangedListener( remainingTimeOrDistanceChangedListener @@ -335,7 +319,7 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven if (roadSnappedLocationListener != null) { disableRoadSnappedLocationUpdates() } - if (turnByTurnEventsEnabled) { + if (navInfoObserver != null) { disableTurnByTurnNavigationEvents() } } @@ -561,15 +545,6 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven } } - /** - * Check if navigation session is already created. - * - * @return true if session is already created. - */ - fun isInitialized(): Boolean { - return navigator != 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)). @@ -577,7 +552,7 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven * @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) } /** @@ -586,7 +561,7 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven */ fun resetTermsAccepted() { try { - NavigationApi.resetTermsAccepted(getActivity().application) + NavigationApi.resetTermsAccepted(application) } catch (error: IllegalStateException) { throw FlutterError( "termsResetNotAllowed", @@ -710,63 +685,36 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven @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 b443bfee..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 @@ -20,25 +20,25 @@ 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 { - private fun manager(): GoogleMapsNavigationSessionManager { - return GoogleMapsNavigationSessionManager.getInstance() - } +class GoogleMapsNavigationSessionMessageHandler( + private val sessionManager: GoogleMapsNavigationSessionManager +) : NavigationSessionApi { 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 GoogleMapsNavigatorHolder.getInitializationState() == + GoogleNavigatorInitializationState.INITIALIZED } - override fun cleanup() { - manager().cleanup() + override fun cleanup(resetSession: Boolean) { + sessionManager.cleanup(resetSession) } override fun showTermsAndConditionsDialog( @@ -47,37 +47,36 @@ class GoogleMapsNavigationSessionMessageHandler : NavigationSessionApi { shouldOnlyShowDriverAwarenessDisclaimer: Boolean, callback: (Result) -> Unit, ) { - manager() - .showTermsAndConditionsDialog( - title, - companyName, - shouldOnlyShowDriverAwarenessDisclaimer, - callback, - ) + sessionManager.showTermsAndConditionsDialog( + title, + companyName, + shouldOnlyShowDriverAwarenessDisclaimer, + callback, + ) } 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( @@ -93,7 +92,7 @@ class GoogleMapsNavigationSessionMessageHandler : NavigationSessionApi { } else { RoutingOptions() } - manager().setDestinations( + sessionManager.setDestinations( waypoints, routingOptions, displayOptions, @@ -111,11 +110,11 @@ class GoogleMapsNavigationSessionMessageHandler : NavigationSessionApi { } 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 { @@ -124,32 +123,32 @@ class GoogleMapsNavigationSessionMessageHandler : NavigationSessionApi { } 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) } @@ -157,22 +156,21 @@ class GoogleMapsNavigationSessionMessageHandler : NavigationSessionApi { } 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() - .simulateLocationsAlongExistingRouteWithOptions( - SimulationOptions().speedMultiplier(options.speedMultiplier.toFloat()) - ) + sessionManager.simulateLocationsAlongExistingRouteWithOptions( + SimulationOptions().speedMultiplier(options.speedMultiplier.toFloat()) + ) } override fun simulateLocationsAlongNewRoute( @@ -180,7 +178,7 @@ class GoogleMapsNavigationSessionMessageHandler : NavigationSessionApi { 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 { @@ -198,7 +196,7 @@ class GoogleMapsNavigationSessionMessageHandler : NavigationSessionApi { callback: (Result) -> Unit, ) { val convertedWaypoints = waypoints.map { Convert.convertWaypointFromDto(it) } - manager().simulateLocationsAlongNewRouteWithRoutingOptions( + sessionManager.simulateLocationsAlongNewRouteWithRoutingOptions( convertedWaypoints, Convert.convertRoutingOptionsFromDto(routingOptions), ) { @@ -220,7 +218,7 @@ class GoogleMapsNavigationSessionMessageHandler : NavigationSessionApi { callback: (Result) -> Unit, ) { val convertedWaypoints = waypoints.map { Convert.convertWaypointFromDto(it) } - manager().simulateLocationsAlongNewRouteWithRoutingAndSimulationOptions( + sessionManager.simulateLocationsAlongNewRouteWithRoutingAndSimulationOptions( convertedWaypoints, Convert.convertRoutingOptionsFromDto(routingOptions), SimulationOptions().speedMultiplier(simulationOptions.speedMultiplier.toFloat()), @@ -237,11 +235,11 @@ class GoogleMapsNavigationSessionMessageHandler : NavigationSessionApi { } override fun pauseSimulation() { - manager().pauseSimulation() + sessionManager.pauseSimulation() } override fun resumeSimulation() { - manager().resumeSimulation() + sessionManager.resumeSimulation() } override fun allowBackgroundLocationUpdates(allow: Boolean) { @@ -249,29 +247,28 @@ class GoogleMapsNavigationSessionMessageHandler : NavigationSessionApi { } 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() - .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 605c2828..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 @@ -65,12 +65,11 @@ internal constructor( // Initialize navigation view with given navigation view options var navigationViewEnabled = false if ( - navigationOptions?.navigationUiEnabledPreference == NavigationUIEnabledPreference.AUTOMATIC + navigationOptions?.navigationUiEnabledPreference == NavigationUIEnabledPreference.AUTOMATIC && + GoogleMapsNavigatorHolder.getInitializationState() == + GoogleNavigatorInitializationState.INITIALIZED ) { - val navigatorInitialized = GoogleMapsNavigationSessionManager.getInstance().isInitialized() - if (navigatorInitialized) { - navigationViewEnabled = true - } + navigationViewEnabled = true } _navigationView.isNavigationUiEnabled = navigationViewEnabled 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..37371160 --- /dev/null +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigatorHolder.kt @@ -0,0 +1,157 @@ +/* + * 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 + +/** + * 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() + + // Turn-by-turn navigation service management + private var turnByTurnServiceRegistered = false + private val navInfoObservers = 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 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/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( 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/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 @@ + + + + + + + + + + ) -> 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);