Skip to content

feat(mobile): show OpenMapX navigation on Android Auto - #776

Merged
Medformatik merged 23 commits into
mainfrom
feat/android-auto
Nov 4, 2026
Merged

Medformatik merged 23 commits into
mainfrom
feat/android-auto

Conversation

@Medformatik

Copy link
Copy Markdown
Collaborator

What changed and why

The Android app can now be used for ground navigation on Android Auto. Until now, connecting the phone to a car showed nothing from OpenMapX. Now OpenMapX appears as a navigation app. From the car screen you can search or choose a saved place, compare up to three lettered routes (A, B, C), start guidance and end it with the host's X. The car map is the phone's own map. The native module draws a /car page of the web app on the host surface, so cartography, the road-conditions layer, route lines and route letters stay identical to the phone. Lists, search, preview, the routing card and the trip card use Android Auto's host templates, which is what Google requires for driving apps.

Route preview on Android Auto

Guidance runs through the trip the phone already manages: the same session, reroutes, notification card and recovery. The car is another view of that trip, not a second navigation engine. The routing card, lane strip and instrument cluster use the shared core helpers (maneuverIconName, presentGuidanceLanes), so turn arrows and lanes match the phone banner. The trip's existing navigation notification is extended for the car through the navigation-surfaces hook from #773, so no second card is posted. The map controls follow the host's map action strip: pan, zoom and recenter, plus a speed bubble while driving. Rows in the route list are lettered to match the labels on the map.

Guidance on Android Auto

Routing cards with lane guidance during a drive

While navigating, the road-conditions overlay now shows only events within 1 km of the route (isNearRoute), on the phone and on the car alike. Outside navigation the overlay stays complete. Play reviewers can trigger Android Auto's test-drive intent, which replays a fixed drive in Berlin wherever they are, because openmapx.com's routing and search cover Germany only. The Test drive button on the car screen exists in debug builds only.

The car path reuses shared code instead of reimplementing it in Kotlin or in the car page: distance rounding, geocoding, simulated positions, route framing and letter placement, and the turn-type mapping. The Android Auto developer page documents the architecture, the car-surface protocol, Desktop Head Unit testing and the release gates.

Related work

Refs #406 (Android Auto part; CarPlay is not included). Builds on #773 (navigation surfaces) and #775 (AbortSignal in React Native).

Verification

  • Pixel phone with Android Auto 18.1 and the Desktop Head Unit, on this branch's final build:
    • Destinations and search;
    • lettered alternatives in a wide view and in a narrow split screen;
    • start and guidance with lanes;
    • zoom, pan and recenter;
    • ending the trip with the host's X, after which the real location resumes;
    • Play's test-drive intent;
    • recovery after a head-unit restart, a process kill and a force stop.
  • pnpm lint (Biome, Prettier, translations), pnpm check-types, commitlint.
  • Mobile:
    • apps/mobile tests and type check;
    • assert-boundary, mobile:prebuild:check, assert-permissions, inventory and mobile:bundle:check;
    • permission compliance now counts the car-app permissions.
  • Native: Gradle unit tests for openmapx-android-auto (car state, mapping, session mailbox) and the navigation-surfaces module; :app:assembleDebug.
  • Web: pnpm test --project web, 626 files and 5,922 tests passed.
  • Node: pnpm test --project node and the core navigation tests, including the corridor's bounds for huge or malformed feed geometry.
    • Four unrelated tests (ops-agent concurrency, codegen) timed out under full-suite load and passed when run alone.
  • Policy and build checks:
    • check:policy, production audits, image size, Docker context secrets, ops authority and OpenAPI;
    • pnpm build (including the /car route), packed packages and the docs build.

Not verified: rerouting and the notification tap on this final build; real head units and wireless projection; rotary controllers, including whether the host's end-trip control can be reached with one; hosts older than Android Auto 18.1; Play car review.

Release and upgrade impact

No package release: the only shared-core changes are navigation helpers and formatting exports used by the web app and the mobile shell. @openmapx/extension-cli built from origin/main and from this branch produces byte-identical dist/ files.

The Android app gains the Android Auto car-app service and the androidx.car.app permissions, which take effect with the next app build. Before store metadata claims Android Auto support, Play car review and real head-unit qualification must both pass (see the developer page). No server, database or operator changes.

Work in progress: native car app, car map page, destinations and car start.
Testing on a phone with the Desktop Head Unit showed that car planning and
starting failed and that the car map lacked basic controls.

- Planning and starting work with the phone app in the background: start
  eligibility uses the process importance the car host gives, and expo-location
  is told the process is foreground while the car shows OpenMapX.
- React Native's AbortSignal gets throwIfAborted, which the shared API client
  needs for every directions request.
- Car starts no longer call crypto.randomUUID, which Hermes lacks.
- The route preview opens: the screen push moved out of onGetTemplate.
- Each planning failure shows its own message; only refusals the phone can fix
  ask for phone setup.
- Route preview rows show time and distance with A/B/C badges matching the map,
  a row tap re-highlights its route, the selected route is drawn on top, and
  Start sits in the action strip. Zoom controls also show on the preview.
- Map controls (pan, zoom, overview or recenter) and Destinations stay on
  screen. Android Auto's own X ends the trip, so there is no separate Stop.
- Before a trip the map shows the car's position at street zoom.
- Current speed sits bottom right beside the speed limit, larger, and is hidden
  while choosing a route.
- Turn arrows, keep-left/right forks and lane images are drawn for the routing
  card; guidance distances are rounded; turn alerts only pop up while another
  app is on the car screen.
- Search ranks places near the car first.
- Ending a trip clears its route from the car map.
- Road shields scale with their sign instead of overflowing it.
Google requires navigation apps to simulate a drive when Android Auto calls
NavigationManagerCallback.onAutoDriveEnabled (car app quality NF-7). The car's
Test drive action only exists in debug builds, so release builds had none.

The callback now switches the car session into a test-drive mode. Planning
starts from a fixed point south of Alexanderplatz instead of the phone's
position, search ranks results around it, and Destinations offers the
Brandenburg Gate. The official instance covers Germany only, so a reviewer
anywhere still gets a route. A trip started in this mode replays its route
through the simulated location driver, installed before the start so no real
location service runs. Only the host callback can enable this, and it ends with
the car session.

Also centre the A badge when the preview has a single route.
The car drew its own arrows: lane arrows bent at a shallow angle, so a left
turn looked like a slight left, and they differed from the turn icon beside
them. Lane highlighting also used the raw engine data, while the phone
recommends lanes from signs when the engine marks none, and the car showed
lanes long before the turn.

The phone's glyph choice and lane presentation move from the web app to
@openmapx/core (maneuverIconName, presentGuidanceLanes). The phone's banner
and lane strip now use them unchanged, and the car projection sends the same
glyph names and lane cells. The car draws the same Material icon paths in the
banner's style and shows lanes only within the phone's approach distance.

Also allowlist the car surface in the camera-framing and marker-click gates
and move its route-letter layers to free slot orders.
Android Auto's turn notification was a second CATEGORY_NAVIGATION notification that took over the Expo location notification's ID and restored it on disconnect. The trip already has a navigation card from the navigation-surfaces module, so the car module now decorates that card with a CarAppExtender through a small hook, and re-posts it when the turn or the car connection changes. One notification remains; the Expo notification is left alone.
The car page had a simplified copy of the road-conditions layer that refetched about every half second in follow mode, because the follow camera moves the map every frame, and that drew roadworks even before a trip. It now mounts the phone's RoadConditionsLayer, which has the viewport scheduler, zoom gate and overlay visibility: on while driving through ContextualOverlays, or when the user enabled it.
…e navigation card

Three rules rounded guidance distances: the phone banner (formatMeasurementDistance), the Android Auto module (50 m steps, in Kotlin) and the navigation card (projectSurface). Core now exposes the banner's rule as measurementDistanceParts, also from the navigation subpath the shell imports. The car projection and route list send its value and unit, Kotlin only wraps them as the host's Distance, and the navigation card formats the same parts in the trip's language.
…lyph

The car projection kept its own maneuver list with keep, fork and ramp rules, a second reading of what core's maneuverIconName decides. The host's turn type is now derived from that glyph in one small table; a roundabout's exit number and circling direction travel as their own field.
The car's simulated driver delivered one route vertex per second, so it crawled through curves and leapt along long straights. It now replays core's simulatePositions at 10 m/s, the generator the web's navigation simulator uses, including heading.
…or logic

The car's own label placement picked a geographic point per route. It now uses routePillAnchors, which places the directions map's route pills on visible geometry, apart from each other, and is recomputed whenever the car map settles.
The car page had its own bounds walk; core's geoJsonBBox already serves the phone's route overview. Adds a test for the car overview framing only the rest of the trip.
…and the car

The car built the geocoding URL by hand with fetch. Core now has fetchGeocode, used by the phone's useGeocoding hook and exported from the navigation/api subpath, so the car search goes through the shared API client with the same parameters, proximity rounding, abort and timeout handling.
In split screen Android Auto keeps the map surface full size and reports the list and the media panel as insets, leaving a strip of about 240 px. A fixed 48 px margin on each side shrank the preview to 140 px. The margin now scales with the free area (8 % of its smaller side, 8 to 48 px).
…navigating

During a ground trip the road-conditions overlay showed every incident in view, which covered the map in a city. It now keeps events along the route and up to 1 km beside it (core isNearRoute, built on the route matcher navigation already uses), and shows everything again when the trip ends. The phone and the Android Auto car map both mount this layer, so they behave the same. Outside navigation the overlay is unchanged.
The host's end-trip button was confirmed only on Android Auto 18.1 with touch; rotary access and older hosts need a real car. Head unit restart, process kill and force stop all restore a live car map on the DHU.
- A new car session starts with an empty command mailbox, and the runtime
  ignores a detach that arrives once the next session is connected, so a
  leftover detach or stop cannot end the next session or its trip.
- Trip updates no longer clear a route preview shown during a trip; a trip
  start already sends an empty preview.
- The car saved-places fetch runs only inside the installed Android app that
  asked for car destinations, not for every signed-in web user, and only
  Android advertises car destinations.
- The car renderer's recovery budget resets when the page loads again.
- Planning cancels a search still in flight, so a late result cannot replace
  the plan's result.
- MapCanvas removes its WebGL-loss listener before tearing the map down, so
  its own teardown is not reported to the car host as a renderer failure.
- A failed simulated start gives a still-running trip its real location back,
  and a simulated trip ends when the car disconnects.
- Tapping the turn notification in the car opens the car app.
- The route corridor check samples long lines densely and recognises areas the
  route runs through.
…geometry

Sampling generated interior points before applying its cap, so one very long or malformed line from a feed could allocate millions of points. The spacing is now derived from the total length first, vertices are thinned to a fixed budget, coordinates outside WGS84 ranges are ignored, and the area check is limited in rings and ring size.
…nce surface

The registry lists NAVIGATION_TEMPLATES and ACCESS_SURFACE, which the Android Auto config plugin declares; the test still expected ten permissions and only read app.config.ts.
@Medformatik
Medformatik merged commit b7ea28c into main Nov 4, 2026
28 checks passed
@Medformatik
Medformatik deleted the feat/android-auto branch November 4, 2026 23:49
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant