From 793d8fd29c6c46ff0aca0dc8e99671ed7c7ad93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20V=C3=A4limaa?= Date: Wed, 15 Jul 2026 15:24:26 +0800 Subject: [PATCH 1/3] feat: switch android auto screen to use regular navigation view --- .../react/navsdk/AndroidAutoBaseScreen.java | 84 +++++++++++++------ 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java b/android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java index efdd68a8..f6a71ac2 100644 --- a/android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java +++ b/android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java @@ -37,10 +37,12 @@ import androidx.lifecycle.LifecycleOwner; import com.facebook.proguard.annotations.DoNotStrip; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.UiThreadUtil; import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; -import com.google.android.libraries.navigation.NavigationViewForAuto; +import com.google.android.gms.maps.GoogleMapOptions; +import com.google.android.libraries.navigation.NavigationView; import com.google.android.libraries.navigation.StylingOptions; import org.json.JSONObject; @@ -56,16 +58,17 @@ public abstract class AndroidAutoBaseScreen extends Screen implements SurfaceCallback, INavigationViewController { private static final String VIRTUAL_DISPLAY_NAME = "AndroidAutoNavScreen"; - private NavigationViewForAuto mNavigationView; + private NavigationView mNavigationView; private VirtualDisplay mVirtualDisplay; private Presentation mPresentation; protected GoogleMap mGoogleMap; - protected boolean mNavigationInitialized = false; + protected volatile boolean mNavigationInitialized = false; private MapViewController mMapViewController; + private boolean mIsSurfaceDestroyed = true; private boolean mAndroidAutoModuleInitialized = false; private boolean mNavModuleInitialized = false; - private final AndroidAutoBaseScreen screenInstance = this; + private final NavModule.NavigationReadyListener mNavigationReadyListener = this::onSessionAttached; @Override public void setStylingOptions(StylingOptions stylingOptions) { @@ -78,16 +81,26 @@ public void setStylingOptions(StylingOptions stylingOptions) { */ private void onSessionAttached(boolean ready) { mNavigationInitialized = ready; + updateNavigationUiEnabled(ready); onNavigationReady(ready); } + private void updateNavigationUiEnabled(boolean ready) { + UiThreadUtil.runOnUiThread( + () -> { + NavigationView navigationView = mNavigationView; + if (!mIsSurfaceDestroyed && navigationView != null) { + navigationView.setNavigationUiEnabled(ready); + } + }); + } + /** * Called when the navigation session state changes. Override this method in your subclass to * handle navigation ready state changes. * - *

Note: Navigation UI controls like setHeaderEnabled, setFooterEnabled, - * setSpeedometerEnabled, etc. are NOT supported on Android Auto's NavigationViewForAuto. These - * controls are automatically managed by the Android Auto navigation template. + *

The Android Auto map view disables phone-oriented navigation UI controls so that the + * Android Auto navigation template remains the sole provider of navigation UI. * *

The navigation state ({@code mNavigationInitialized}) is already updated before this method * is called. @@ -95,9 +108,6 @@ private void onSessionAttached(boolean ready) { * @param ready true when navigation session is ready, false when it's no longer available. */ protected void onNavigationReady(boolean ready) { - // NavigationViewForAuto does not support direct UI control settings like - // setHeaderEnabled, setFooterEnabled, setTrafficPromptsEnabled, etc. - // These are automatically managed by the Android Auto navigation template. // Override this method in your subclass if you need custom behavior. } @@ -115,7 +125,7 @@ public AndroidAutoBaseScreen(@NonNull CarContext carContext) { () -> { mNavModuleInitialized = true; try { - NavModule.getInstance().registerNavigationReadyListener(this::onSessionAttached); + NavModule.getInstance().registerNavigationReadyListener(mNavigationReadyListener); } catch (IllegalStateException e) { // NavModule not yet initialized, will be registered later } @@ -134,7 +144,7 @@ public void onDestroy(@NonNull LifecycleOwner lifecycleOwner) { if (mNavModuleInitialized) { try { NavModule.getInstance() - .unRegisterNavigationReadyListener(screenInstance::onSessionAttached); + .unRegisterNavigationReadyListener(mNavigationReadyListener); } catch (Exception e) { // Module may have been destroyed, safe to ignore. } @@ -175,6 +185,7 @@ public void onSurfaceAvailable(@NonNull SurfaceContainer surfaceContainer) { if (!isSurfaceReady(surfaceContainer)) { return; } + mIsSurfaceDestroyed = false; mVirtualDisplay = getCarContext() .getSystemService(DisplayManager.class) @@ -187,16 +198,31 @@ public void onSurfaceAvailable(@NonNull SurfaceContainer surfaceContainer) { DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY); mPresentation = new Presentation(getCarContext(), mVirtualDisplay.getDisplay()); - mNavigationView = new NavigationViewForAuto(getCarContext()); + GoogleMapOptions googleMapOptions = new GoogleMapOptions().compassEnabled(false); + mNavigationView = new NavigationView(getCarContext(), googleMapOptions); mNavigationView.onCreate(null); mNavigationView.onStart(); mNavigationView.onResume(); + mNavigationView.setHeaderEnabled(false); + mNavigationView.setRecenterButtonEnabled(false); + mNavigationView.setEtaCardEnabled(false); + mNavigationView.setSpeedometerEnabled(false); + mNavigationView.setTripProgressBarEnabled(false); + mNavigationView.setReportIncidentButtonEnabled(false); + mNavigationView.setNavigationUiEnabled(mNavigationInitialized); mPresentation.setContentView(mNavigationView); mPresentation.show(); - mNavigationView.getMapAsync( + NavigationView navigationView = mNavigationView; + navigationView.getMapAsync( (GoogleMap googleMap) -> { + if (mIsSurfaceDestroyed || navigationView != mNavigationView) { + return; + } + + googleMap.getUiSettings().setIndoorLevelPickerEnabled(false); + googleMap.getUiSettings().setMyLocationButtonEnabled(false); mGoogleMap = googleMap; mMapViewController = new MapViewController(); mMapViewController.initialize(googleMap, () -> null); @@ -210,27 +236,35 @@ public void onSurfaceAvailable(@NonNull SurfaceContainer surfaceContainer) { * Called when the map view has been loaded and is ready. Override this method in your subclass to * configure map settings. * - *

Note: Navigation UI controls like setSpeedometerEnabled, setSpeedLimitIconEnabled, - * etc. are NOT supported on Android Auto's NavigationViewForAuto. These controls are - * automatically managed by the Android Auto navigation template. + *

Phone-oriented navigation UI controls are disabled on the map view to avoid overlapping + * the Android Auto navigation template. */ protected void onMapViewReady() { - // NavigationViewForAuto does not support direct UI control settings like - // setSpeedometerEnabled, setSpeedLimitIconEnabled, etc. - // These are automatically managed by the Android Auto navigation template. // Override this method in your subclass if you need custom behavior. } @Override public void onSurfaceDestroyed(@NonNull SurfaceContainer surfaceContainer) { + mIsSurfaceDestroyed = true; unRegisterControllersForAndroidAutoModule(); - mNavigationView.onPause(); - mNavigationView.onStop(); - mNavigationView.onDestroy(); + mMapViewController = null; + + if (mNavigationView != null) { + mNavigationView.onPause(); + mNavigationView.onStop(); + mNavigationView.onDestroy(); + mNavigationView = null; + } mGoogleMap = null; - mPresentation.dismiss(); - mVirtualDisplay.release(); + if (mPresentation != null) { + mPresentation.dismiss(); + mPresentation = null; + } + if (mVirtualDisplay != null) { + mVirtualDisplay.release(); + mVirtualDisplay = null; + } } @Override From 06e98f7b61372c51d68092d318e0f300aea0a3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20V=C3=A4limaa?= Date: Thu, 16 Jul 2026 15:12:16 +0800 Subject: [PATCH 2/3] feat: add isPromptVisible to android auto base screen --- ANDROIDAUTO.md | 26 ++++++++++++ .../react/navsdk/AndroidAutoBaseScreen.java | 42 ++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/ANDROIDAUTO.md b/ANDROIDAUTO.md index e7d32028..acdd0b02 100644 --- a/ANDROIDAUTO.md +++ b/ANDROIDAUTO.md @@ -61,6 +61,32 @@ public Template onGetTemplate() { } ``` +### Avoiding overlap with Navigation SDK prompts + +`AndroidAutoBaseScreen` detects Navigation SDK prompts, including traffic and rerouting prompts, +and automatically invalidates the Android Auto template when their visibility changes. If your +custom `NavigationTemplate` includes a `TravelEstimate`, conditionally omit it while a prompt is +visible. This prevents the host-controlled estimate from overlapping the Navigation SDK modal +prompt: + +```java +@Override +public Template onGetTemplate() { + NavigationTemplate.Builder builder = new NavigationTemplate.Builder(); + // Configure actions, map action strip, and navigation info. + + if (!isPromptVisible()) { + builder.setDestinationTravelEstimate(travelEstimate); + } + + return builder.build(); +} +``` + +`isPromptVisible()` is provided by `AndroidAutoBaseScreen`; subclasses do not need to register a +listener or call `invalidate()` themselves. The estimate is restored automatically when the prompt +disappears. + For advanced customization, you can bypass the base class and implement your own screen by inheriting `Screen`. You can use the provided `AndroidAutoBaseScreen` base class as a reference on how to do that. ### React Native specific setup diff --git a/android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java b/android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java index f6a71ac2..db3a8bbe 100644 --- a/android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java +++ b/android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java @@ -43,6 +43,7 @@ import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.GoogleMapOptions; import com.google.android.libraries.navigation.NavigationView; +import com.google.android.libraries.navigation.PromptVisibilityChangedListener; import com.google.android.libraries.navigation.StylingOptions; import org.json.JSONObject; @@ -64,7 +65,9 @@ public abstract class AndroidAutoBaseScreen extends Screen protected GoogleMap mGoogleMap; protected volatile boolean mNavigationInitialized = false; private MapViewController mMapViewController; + private PromptVisibilityChangedListener mPromptVisibilityChangedListener; private boolean mIsSurfaceDestroyed = true; + private boolean mIsPromptVisible = false; private boolean mAndroidAutoModuleInitialized = false; private boolean mNavModuleInitialized = false; @@ -95,6 +98,31 @@ private void updateNavigationUiEnabled(boolean ready) { }); } + private void handlePromptVisibilityChanged( + NavigationView navigationView, boolean promptVisible) { + UiThreadUtil.runOnUiThread( + () -> { + if (mIsSurfaceDestroyed + || navigationView != mNavigationView + || mIsPromptVisible == promptVisible) { + return; + } + + mIsPromptVisible = promptVisible; + onPromptVisibilityChanged(promptVisible); + }); + } + + /** Returns whether a Navigation SDK prompt is visible over the Android Auto map. */ + protected final boolean isPromptVisible() { + return mIsPromptVisible; + } + + /** Called when Navigation SDK prompt visibility changes on the Android Auto map. */ + protected void onPromptVisibilityChanged(boolean promptVisible) { + invalidate(); + } + /** * Called when the navigation session state changes. Override this method in your subclass to * handle navigation ready state changes. @@ -211,10 +239,14 @@ public void onSurfaceAvailable(@NonNull SurfaceContainer surfaceContainer) { mNavigationView.setReportIncidentButtonEnabled(false); mNavigationView.setNavigationUiEnabled(mNavigationInitialized); - mPresentation.setContentView(mNavigationView); + NavigationView navigationView = mNavigationView; + mPromptVisibilityChangedListener = + promptVisible -> handlePromptVisibilityChanged(navigationView, promptVisible); + navigationView.addPromptVisibilityChangedListener(mPromptVisibilityChangedListener); + + mPresentation.setContentView(navigationView); mPresentation.show(); - NavigationView navigationView = mNavigationView; navigationView.getMapAsync( (GoogleMap googleMap) -> { if (mIsSurfaceDestroyed || navigationView != mNavigationView) { @@ -249,12 +281,18 @@ public void onSurfaceDestroyed(@NonNull SurfaceContainer surfaceContainer) { unRegisterControllersForAndroidAutoModule(); mMapViewController = null; + if (mNavigationView != null && mPromptVisibilityChangedListener != null) { + mNavigationView.removePromptVisibilityChangedListener(mPromptVisibilityChangedListener); + } + mPromptVisibilityChangedListener = null; + if (mNavigationView != null) { mNavigationView.onPause(); mNavigationView.onStop(); mNavigationView.onDestroy(); mNavigationView = null; } + mIsPromptVisible = false; mGoogleMap = null; if (mPresentation != null) { From fae943af428a36ab9af1151fcd4b724981d37171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20V=C3=A4limaa?= Date: Thu, 16 Jul 2026 15:18:51 +0800 Subject: [PATCH 3/3] chore: formatting --- .../react/navsdk/AndroidAutoBaseScreen.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java b/android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java index db3a8bbe..44ce50ab 100644 --- a/android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java +++ b/android/src/main/java/com/google/android/react/navsdk/AndroidAutoBaseScreen.java @@ -71,7 +71,8 @@ public abstract class AndroidAutoBaseScreen extends Screen private boolean mAndroidAutoModuleInitialized = false; private boolean mNavModuleInitialized = false; - private final NavModule.NavigationReadyListener mNavigationReadyListener = this::onSessionAttached; + private final NavModule.NavigationReadyListener mNavigationReadyListener = + this::onSessionAttached; @Override public void setStylingOptions(StylingOptions stylingOptions) { @@ -98,8 +99,7 @@ private void updateNavigationUiEnabled(boolean ready) { }); } - private void handlePromptVisibilityChanged( - NavigationView navigationView, boolean promptVisible) { + private void handlePromptVisibilityChanged(NavigationView navigationView, boolean promptVisible) { UiThreadUtil.runOnUiThread( () -> { if (mIsSurfaceDestroyed @@ -127,8 +127,8 @@ protected void onPromptVisibilityChanged(boolean promptVisible) { * Called when the navigation session state changes. Override this method in your subclass to * handle navigation ready state changes. * - *

The Android Auto map view disables phone-oriented navigation UI controls so that the - * Android Auto navigation template remains the sole provider of navigation UI. + *

The Android Auto map view disables phone-oriented navigation UI controls so that the Android + * Auto navigation template remains the sole provider of navigation UI. * *

The navigation state ({@code mNavigationInitialized}) is already updated before this method * is called. @@ -171,8 +171,7 @@ public AndroidAutoBaseScreen(@NonNull CarContext carContext) { public void onDestroy(@NonNull LifecycleOwner lifecycleOwner) { if (mNavModuleInitialized) { try { - NavModule.getInstance() - .unRegisterNavigationReadyListener(mNavigationReadyListener); + NavModule.getInstance().unRegisterNavigationReadyListener(mNavigationReadyListener); } catch (Exception e) { // Module may have been destroyed, safe to ignore. } @@ -268,8 +267,8 @@ public void onSurfaceAvailable(@NonNull SurfaceContainer surfaceContainer) { * Called when the map view has been loaded and is ready. Override this method in your subclass to * configure map settings. * - *

Phone-oriented navigation UI controls are disabled on the map view to avoid overlapping - * the Android Auto navigation template. + *

Phone-oriented navigation UI controls are disabled on the map view to avoid overlapping the + * Android Auto navigation template. */ protected void onMapViewReady() { // Override this method in your subclass if you need custom behavior.