diff --git a/ANDROIDAUTO.md b/ANDROIDAUTO.md index e7d3202..acdd0b0 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 efdd68a..44ce50a 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,13 @@ 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.PromptVisibilityChangedListener; import com.google.android.libraries.navigation.StylingOptions; import org.json.JSONObject; @@ -56,16 +59,20 @@ 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 PromptVisibilityChangedListener mPromptVisibilityChangedListener; + private boolean mIsSurfaceDestroyed = true; + private boolean mIsPromptVisible = false; 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 +85,50 @@ 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); + } + }); + } + + 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. * - *

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 +136,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 +153,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 } @@ -133,8 +171,7 @@ public AndroidAutoBaseScreen(@NonNull CarContext carContext) { public void onDestroy(@NonNull LifecycleOwner lifecycleOwner) { if (mNavModuleInitialized) { try { - NavModule.getInstance() - .unRegisterNavigationReadyListener(screenInstance::onSessionAttached); + NavModule.getInstance().unRegisterNavigationReadyListener(mNavigationReadyListener); } catch (Exception e) { // Module may have been destroyed, safe to ignore. } @@ -175,6 +212,7 @@ public void onSurfaceAvailable(@NonNull SurfaceContainer surfaceContainer) { if (!isSurfaceReady(surfaceContainer)) { return; } + mIsSurfaceDestroyed = false; mVirtualDisplay = getCarContext() .getSystemService(DisplayManager.class) @@ -187,16 +225,35 @@ 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); + NavigationView navigationView = mNavigationView; + mPromptVisibilityChangedListener = + promptVisible -> handlePromptVisibilityChanged(navigationView, promptVisible); + navigationView.addPromptVisibilityChangedListener(mPromptVisibilityChangedListener); + + mPresentation.setContentView(navigationView); mPresentation.show(); - mNavigationView.getMapAsync( + 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 +267,41 @@ 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 && mPromptVisibilityChangedListener != null) { + mNavigationView.removePromptVisibilityChangedListener(mPromptVisibilityChangedListener); + } + mPromptVisibilityChangedListener = null; + + if (mNavigationView != null) { + mNavigationView.onPause(); + mNavigationView.onStop(); + mNavigationView.onDestroy(); + mNavigationView = null; + } + mIsPromptVisible = false; mGoogleMap = null; - mPresentation.dismiss(); - mVirtualDisplay.release(); + if (mPresentation != null) { + mPresentation.dismiss(); + mPresentation = null; + } + if (mVirtualDisplay != null) { + mVirtualDisplay.release(); + mVirtualDisplay = null; + } } @Override