Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -71,7 +71,7 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) :
null
private var speedingListener: SpeedingListener? = null
private var weakActivity: WeakReference<Activity>? = null
private var turnByTurnEventsEnabled: Boolean = false
private var navInfoObserver: Observer<NavInfo>? = null
private var weakLifecycleOwner: WeakReference<LifecycleOwner>? = null
private var taskRemovedBehavior: @TaskRemovedBehavior Int = 0

Expand Down Expand Up @@ -221,7 +221,7 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) :
}
}

NavigationApi.getNavigator(getActivity(), listener)
NavigationApi.getNavigator(application, listener)
}

private fun convertNavigatorErrorToFlutterError(
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -324,7 +319,7 @@ constructor(private val navigationSessionEventApi: NavigationSessionEventApi) :
if (roadSnappedLocationListener != null) {
disableRoadSnappedLocationUpdates()
}
if (turnByTurnEventsEnabled) {
if (navInfoObserver != null) {
disableTurnByTurnNavigationEvents()
}
}
Expand Down Expand Up @@ -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)
}

/**
Expand All @@ -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",
Expand Down Expand Up @@ -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<NavInfo> = 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()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ class GoogleMapsNavigationSessionMessageHandler(
GoogleNavigatorInitializationState.INITIALIZED
}

override fun cleanup() {
sessionManager.cleanup()
override fun cleanup(resetSession: Boolean) {
sessionManager.cleanup(resetSession)
}

override fun showTermsAndConditionsDialog(
Expand Down
Original file line number Diff line number Diff line change
@@ -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

/**
Expand All @@ -18,6 +40,10 @@ object GoogleMapsNavigatorHolder {
private var initializationState = GoogleNavigatorInitializationState.NOT_INITIALIZED
private val initializationCallbacks = mutableListOf<NavigationApi.NavigatorListener>()

// Turn-by-turn navigation service management
private var turnByTurnServiceRegistered = false
private val navInfoObservers = mutableListOf<Observer<NavInfo>>()

@Synchronized fun getNavigator(): Navigator? = navigator

@Synchronized
Expand Down Expand Up @@ -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<NavInfo>) {
if (!navInfoObservers.contains(observer)) {
navInfoObservers.add(observer)
GoogleMapsNavigationNavUpdatesService.navInfoLiveData.observeForever(observer)
}
}

@Synchronized
fun removeNavInfoObserver(observer: Observer<NavInfo>) {
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5702,7 +5702,7 @@ interface NavigationSessionApi {

fun isInitialized(): Boolean

fun cleanup()
fun cleanup(resetSession: Boolean)

fun showTermsAndConditionsDialog(
title: String,
Expand Down Expand Up @@ -5862,10 +5862,12 @@ interface NavigationSessionApi {
codec,
)
if (api != null) {
channel.setMessageHandler { _, reply ->
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val resetSessionArg = args[0] as Boolean
val wrapped: List<Any?> =
try {
api.cleanup()
api.cleanup(resetSessionArg)
listOf(null)
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
Expand Down
Loading
Loading