Skip to content

Commit bac236d

Browse files
committed
feat: support prompt visibility handling on Android Auto and CarPlay views
1 parent c165694 commit bac236d

File tree

16 files changed

+736
-5
lines changed

16 files changed

+736
-5
lines changed

android/src/main/kotlin/com/google/maps/flutter/navigation/AndroidAutoBaseScreen.kt

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import com.google.android.gms.maps.CameraUpdateFactory
3636
import com.google.android.gms.maps.GoogleMap
3737
import com.google.android.gms.maps.GoogleMapOptions
3838
import com.google.android.libraries.navigation.NavigationViewForAuto
39+
import com.google.android.libraries.navigation.PromptVisibilityChangedListener
3940

4041
open class AndroidAutoBaseScreen(carContext: CarContext) :
4142
Screen(carContext), SurfaceCallback, NavigationReadyListener {
@@ -45,6 +46,7 @@ open class AndroidAutoBaseScreen(carContext: CarContext) :
4546
private var mNavigationView: NavigationViewForAuto? = null
4647
private var mAutoMapView: GoogleMapsAutoMapView? = null
4748
private var mViewRegistry: GoogleMapsViewRegistry? = null
49+
private var mPromptVisibilityListener: PromptVisibilityChangedListener? = null
4850
protected var mIsNavigationReady: Boolean = false
4951
var mGoogleMap: GoogleMap? = null
5052

@@ -124,6 +126,13 @@ open class AndroidAutoBaseScreen(carContext: CarContext) :
124126
navigationView,
125127
googleMap,
126128
)
129+
130+
// Set up prompt visibility listener
131+
mPromptVisibilityListener = PromptVisibilityChangedListener { promptVisible ->
132+
onPromptVisibilityChanged(promptVisible)
133+
}
134+
navigationView.addPromptVisibilityChangedListener(mPromptVisibilityListener)
135+
127136
sendAutoScreenAvailabilityChangedEvent(true)
128137
invalidate()
129138
}
@@ -133,6 +142,13 @@ open class AndroidAutoBaseScreen(carContext: CarContext) :
133142
override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) {
134143
super.onSurfaceDestroyed(surfaceContainer)
135144
sendAutoScreenAvailabilityChangedEvent(false)
145+
146+
// Clean up prompt visibility listener
147+
if (mPromptVisibilityListener != null) {
148+
mNavigationView?.removePromptVisibilityChangedListener(mPromptVisibilityListener)
149+
mPromptVisibilityListener = null
150+
}
151+
136152
mViewRegistry?.unregisterAndroidAutoView()
137153
mNavigationView?.onPause()
138154
mNavigationView?.onStop()
@@ -166,6 +182,14 @@ open class AndroidAutoBaseScreen(carContext: CarContext) :
166182
) {}
167183
}
168184

185+
// Called when Flutter sends a custom event to native via sendCustomNavigationAutoEvent
186+
// Override this method in your AndroidAutoBaseScreen subclass to handle custom events from
187+
// Flutter
188+
open fun onCustomNavigationAutoEventFromFlutter(event: String, data: Any) {
189+
// Default implementation does nothing
190+
// Subclasses can override to handle custom events
191+
}
192+
169193
private fun sendAutoScreenAvailabilityChangedEvent(isAvailable: Boolean) {
170194
GoogleMapsNavigationPlugin.getInstance()?.autoViewEventApi?.onAutoScreenAvailabilityChanged(
171195
isAvailable
@@ -175,4 +199,57 @@ open class AndroidAutoBaseScreen(carContext: CarContext) :
175199
override fun onNavigationReady(ready: Boolean) {
176200
mIsNavigationReady = ready
177201
}
202+
203+
/**
204+
* Checks if a traffic prompt is currently visible on the Android Auto screen.
205+
*
206+
* This can be useful to dynamically adjust your UI based on prompt visibility, such as when
207+
* building templates or deciding whether to show custom elements.
208+
*
209+
* @return true if a prompt is currently visible, false otherwise
210+
*
211+
* Example:
212+
* ```kotlin
213+
* override fun onGetTemplate(): Template {
214+
* val builder = NavigationTemplate.Builder()
215+
*
216+
* // Only show custom actions if prompt is not visible
217+
* if (!isPromptVisible()) {
218+
* builder.setActionStrip(myCustomActionStrip)
219+
* }
220+
*
221+
* return builder.build()
222+
* }
223+
* ```
224+
*/
225+
fun isPromptVisible(): Boolean {
226+
return mNavigationView?.isPromptVisible ?: false
227+
}
228+
229+
/**
230+
* Called when traffic prompt visibility changes on the Android Auto screen.
231+
*
232+
* Override this method to add custom behavior when prompts appear or disappear, such as
233+
* hiding/showing your custom UI elements to avoid overlapping with system prompts.
234+
*
235+
* @param promptVisible true if the prompt is now visible, false if it's hidden
236+
*
237+
* Example:
238+
* ```kotlin
239+
* override fun onPromptVisibilityChanged(promptVisible: Boolean) {
240+
* super.onPromptVisibilityChanged(promptVisible)
241+
* if (promptVisible) {
242+
* // Hide your custom buttons or UI elements
243+
* } else {
244+
* // Show your custom buttons or UI elements
245+
* }
246+
* }
247+
* ```
248+
*/
249+
open fun onPromptVisibilityChanged(promptVisible: Boolean) {
250+
// Send event to Flutter by default
251+
GoogleMapsNavigationPlugin.getInstance()?.autoViewEventApi?.onPromptVisibilityChanged(
252+
promptVisible
253+
) {}
254+
}
178255
}

android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsAutoMapView.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ internal constructor(
2828
private val mapView: NavigationViewForAuto,
2929
map: GoogleMap,
3030
) : GoogleMapsBaseMapView(null, mapOptions, null, imageRegistry) {
31+
private var _isTrafficPromptsEnabled: Boolean = true
32+
3133
override fun getView(): View {
3234
return mapView
3335
}
@@ -40,6 +42,15 @@ internal constructor(
4042
mapReady()
4143
}
4244

45+
fun setTrafficPromptsEnabled(enabled: Boolean) {
46+
mapView.setTrafficPromptsEnabled(enabled)
47+
_isTrafficPromptsEnabled = enabled
48+
}
49+
50+
fun isTrafficPromptsEnabled(): Boolean {
51+
return _isTrafficPromptsEnabled
52+
}
53+
4354
// Handled by AndroidAutoBaseScreen.
4455
override fun onStart(): Boolean {
4556
return super.onStart()

android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsAutoViewMessageHandler.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ class GoogleMapsAutoViewMessageHandler(private val viewRegistry: GoogleMapsViewR
9999
getView().setTrafficEnabled(enabled)
100100
}
101101

102+
override fun setTrafficPromptsEnabled(enabled: Boolean) {
103+
(getView() as? GoogleMapsAutoMapView)?.setTrafficPromptsEnabled(enabled)
104+
?: throw FlutterError(
105+
"invalidViewType",
106+
"setTrafficPromptsEnabled is only supported on GoogleMapsAutoMapView",
107+
)
108+
}
109+
102110
override fun isMyLocationButtonEnabled(): Boolean {
103111
return getView().isMyLocationButtonEnabled()
104112
}
@@ -143,6 +151,14 @@ class GoogleMapsAutoViewMessageHandler(private val viewRegistry: GoogleMapsViewR
143151
return getView().isTrafficEnabled()
144152
}
145153

154+
override fun isTrafficPromptsEnabled(): Boolean {
155+
return (getView() as? GoogleMapsAutoMapView)?.isTrafficPromptsEnabled()
156+
?: throw FlutterError(
157+
"invalidViewType",
158+
"isTrafficPromptsEnabled is only supported on GoogleMapsAutoMapView",
159+
)
160+
}
161+
146162
override fun getMyLocation(): LatLngDto? {
147163
val location = getView().getMyLocation() ?: return null
148164
return LatLngDto(location.latitude, location.longitude)
@@ -393,4 +409,14 @@ class GoogleMapsAutoViewMessageHandler(private val viewRegistry: GoogleMapsViewR
393409
override fun getPadding(): MapPaddingDto {
394410
return getView().getPadding()
395411
}
412+
413+
override fun sendCustomNavigationAutoEvent(event: String, data: Any) {
414+
// This method receives custom events from Flutter.
415+
// The implementation is left empty by design, as developers should handle
416+
// custom events in their AndroidAutoBaseScreen subclass by overriding
417+
// onCustomNavigationAutoEventFromFlutter method.
418+
//
419+
// Note: If you need to handle events here, you would need to maintain a reference
420+
// to your AndroidAutoBaseScreen instance and call a method on it.
421+
}
396422
}

android/src/main/kotlin/com/google/maps/flutter/navigation/messages.g.kt

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7387,6 +7387,8 @@ interface AutoMapViewApi {
73877387

73887388
fun setTrafficEnabled(enabled: Boolean)
73897389

7390+
fun setTrafficPromptsEnabled(enabled: Boolean)
7391+
73907392
fun isMyLocationButtonEnabled(): Boolean
73917393

73927394
fun isConsumeMyLocationButtonClickEventsEnabled(): Boolean
@@ -7409,6 +7411,8 @@ interface AutoMapViewApi {
74097411

74107412
fun isTrafficEnabled(): Boolean
74117413

7414+
fun isTrafficPromptsEnabled(): Boolean
7415+
74127416
fun getMarkers(): List<MarkerDto>
74137417

74147418
fun addMarkers(markers: List<MarkerDto>): List<MarkerDto>
@@ -7459,6 +7463,8 @@ interface AutoMapViewApi {
74597463

74607464
fun getPadding(): MapPaddingDto
74617465

7466+
fun sendCustomNavigationAutoEvent(event: String, data: Any)
7467+
74627468
companion object {
74637469
/** The codec used by AutoMapViewApi. */
74647470
val codec: MessageCodec<Any?> by lazy { messagesPigeonCodec() }
@@ -8415,6 +8421,30 @@ interface AutoMapViewApi {
84158421
channel.setMessageHandler(null)
84168422
}
84178423
}
8424+
run {
8425+
val channel =
8426+
BasicMessageChannel<Any?>(
8427+
binaryMessenger,
8428+
"dev.flutter.pigeon.google_navigation_flutter.AutoMapViewApi.setTrafficPromptsEnabled$separatedMessageChannelSuffix",
8429+
codec,
8430+
)
8431+
if (api != null) {
8432+
channel.setMessageHandler { message, reply ->
8433+
val args = message as List<Any?>
8434+
val enabledArg = args[0] as Boolean
8435+
val wrapped: List<Any?> =
8436+
try {
8437+
api.setTrafficPromptsEnabled(enabledArg)
8438+
listOf(null)
8439+
} catch (exception: Throwable) {
8440+
MessagesPigeonUtils.wrapError(exception)
8441+
}
8442+
reply.reply(wrapped)
8443+
}
8444+
} else {
8445+
channel.setMessageHandler(null)
8446+
}
8447+
}
84188448
run {
84198449
val channel =
84208450
BasicMessageChannel<Any?>(
@@ -8646,6 +8676,27 @@ interface AutoMapViewApi {
86468676
channel.setMessageHandler(null)
86478677
}
86488678
}
8679+
run {
8680+
val channel =
8681+
BasicMessageChannel<Any?>(
8682+
binaryMessenger,
8683+
"dev.flutter.pigeon.google_navigation_flutter.AutoMapViewApi.isTrafficPromptsEnabled$separatedMessageChannelSuffix",
8684+
codec,
8685+
)
8686+
if (api != null) {
8687+
channel.setMessageHandler { _, reply ->
8688+
val wrapped: List<Any?> =
8689+
try {
8690+
listOf(api.isTrafficPromptsEnabled())
8691+
} catch (exception: Throwable) {
8692+
MessagesPigeonUtils.wrapError(exception)
8693+
}
8694+
reply.reply(wrapped)
8695+
}
8696+
} else {
8697+
channel.setMessageHandler(null)
8698+
}
8699+
}
86498700
run {
86508701
val channel =
86518702
BasicMessageChannel<Any?>(
@@ -9208,6 +9259,31 @@ interface AutoMapViewApi {
92089259
channel.setMessageHandler(null)
92099260
}
92109261
}
9262+
run {
9263+
val channel =
9264+
BasicMessageChannel<Any?>(
9265+
binaryMessenger,
9266+
"dev.flutter.pigeon.google_navigation_flutter.AutoMapViewApi.sendCustomNavigationAutoEvent$separatedMessageChannelSuffix",
9267+
codec,
9268+
)
9269+
if (api != null) {
9270+
channel.setMessageHandler { message, reply ->
9271+
val args = message as List<Any?>
9272+
val eventArg = args[0] as String
9273+
val dataArg = args[1] as Any
9274+
val wrapped: List<Any?> =
9275+
try {
9276+
api.sendCustomNavigationAutoEvent(eventArg, dataArg)
9277+
listOf(null)
9278+
} catch (exception: Throwable) {
9279+
MessagesPigeonUtils.wrapError(exception)
9280+
}
9281+
reply.reply(wrapped)
9282+
}
9283+
} else {
9284+
channel.setMessageHandler(null)
9285+
}
9286+
}
92119287
}
92129288
}
92139289
}
@@ -9263,6 +9339,25 @@ class AutoViewEventApi(
92639339
}
92649340
}
92659341
}
9342+
9343+
fun onPromptVisibilityChanged(promptVisibleArg: Boolean, callback: (Result<Unit>) -> Unit) {
9344+
val separatedMessageChannelSuffix =
9345+
if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
9346+
val channelName =
9347+
"dev.flutter.pigeon.google_navigation_flutter.AutoViewEventApi.onPromptVisibilityChanged$separatedMessageChannelSuffix"
9348+
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
9349+
channel.send(listOf(promptVisibleArg)) {
9350+
if (it is List<*>) {
9351+
if (it.size > 1) {
9352+
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
9353+
} else {
9354+
callback(Result.success(Unit))
9355+
}
9356+
} else {
9357+
callback(Result.failure(MessagesPigeonUtils.createConnectionError(channelName)))
9358+
}
9359+
}
9360+
}
92669361
}
92679362

92689363
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */

example/android/app/src/main/kotlin/com/google/maps/flutter/navigation_example/SampleAndroidAutoScreen.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,27 @@ class SampleAndroidAutoScreen(carContext: CarContext): AndroidAutoBaseScreen(car
9797
invalidate()
9898
}
9999

100+
// Example of handling prompt visibility changes
101+
// This is called when traffic prompts appear/disappear on the Android Auto screen
102+
override fun onPromptVisibilityChanged(promptVisible: Boolean) {
103+
super.onPromptVisibilityChanged(promptVisible) // This sends the event to Flutter
104+
android.util.Log.d("SampleAndroidAutoScreen", "Prompt visibility changed to: $promptVisible")
105+
106+
// You can add custom logic here, such as:
107+
// - Hiding/showing custom action buttons when prompts appear
108+
// - Adjusting your template layout
109+
// - Updating custom UI elements
110+
111+
// For example, you might want to refresh the template:
112+
// invalidate()
113+
}
114+
115+
// Example of handling custom events from Flutter
116+
override fun onCustomNavigationAutoEventFromFlutter(event: String, data: Any) {
117+
super.onCustomNavigationAutoEventFromFlutter(event, data)
118+
android.util.Log.d("SampleAndroidAutoScreen", "Received custom event from Flutter: event=$event, data=$data")
119+
}
120+
100121
override fun onGetTemplate(): Template {
101122
if (!mIsNavigationReady) {
102123
return PaneTemplate.Builder(

example/ios/Runner/CarSceneDelegate.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,36 @@ class CarSceneDelegate: BaseCarSceneDelegate {
3535
template.leadingNavigationBarButtons = [customEventButton, recenterButton]
3636
return template
3737
}
38+
39+
// Example of handling custom events from Flutter
40+
override func onCustomNavigationAutoEventFromFlutter(event: String, data: Any) {
41+
NSLog("CarSceneDelegate: Received custom event from Flutter: event=\(event), data=\(data)")
42+
}
43+
44+
// Example of handling prompt visibility changes
45+
override func onPromptVisibilityChanged(promptVisible: Bool) {
46+
// Call super to ensure Flutter receives the event
47+
super.onPromptVisibilityChanged(promptVisible: promptVisible)
48+
49+
NSLog("CarSceneDelegate: onPromptVisibilityChanged called with promptVisible=\(promptVisible)")
50+
51+
// Example: Hide custom UI when prompt appears, show it when prompt disappears
52+
// Uncomment to enable this behavior:
53+
// if promptVisible {
54+
// mapTemplate?.leadingNavigationBarButtons = []
55+
// } else {
56+
// // Restore your custom buttons
57+
// let customEventButton = CPBarButton(title: "Custom Event") { [weak self] _ in
58+
// let data = ["sampleDataKey": "sampleDataContent"]
59+
// self?.sendCustomNavigationAutoEvent(event: "CustomCarPlayEvent", data: data)
60+
// }
61+
// let recenterButton = CPBarButton(title: "Re-center") { [weak self] _ in
62+
// self?.getNavView()?.followMyLocation(
63+
// perspective: GMSNavigationCameraPerspective.tilted,
64+
// zoomLevel: nil
65+
// )
66+
// }
67+
// mapTemplate?.leadingNavigationBarButtons = [customEventButton, recenterButton]
68+
// }
69+
}
3870
}

0 commit comments

Comments
 (0)