Skip to content
This repository was archived by the owner on Aug 25, 2026. It is now read-only.

Android Auto: full car-side navigation + fork feature suite [won't appear in cars without an allowlist workaround - see PimpinPumpkin/Vela#179] - #93

Merged
alltechdev merged 31 commits into
mainfrom
port/android-auto-full-nav
Jul 24, 2026
Merged

alltechdev merged 31 commits into
mainfrom
port/android-auto-full-nav

Conversation

@alltechdev

@alltechdev alltechdev commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Important

⚠️ This will NOT show up in your car by just installing the APK

Android Auto allowlists navigation apps inside the AA host (gearhead) itself, keyed on package name + Play signing certificate. A sideloaded Vela is invisible to a real head unit no matter what this PR does — the full investigation (decompiled gearhead, patched-AA tests on hardware) is in PimpinPumpkin/Vela#179.

To actually see this working in a car you need one of the few known workarounds from that issue — e.g. a rooted phone with an AA allowlist bypass (patched gearhead / module), or the AA "unknown sources" developer path where it still applies. Read #179 before filing "doesn't appear in my car" reports: for a stock, unrooted phone with official Android Auto, this cannot appear, and that is Google's gate, not a Vela bug.

Android Auto, from invisible to a full-featured car experience: the upstream port, then two waves of fork-original features on top of it.

The port

c06de11 - the invisibility fix. Vela never appeared in Android Auto's launcher: the build shipped only the base androidx.car.app:app library, and the AA host discovers car apps through the CarAppMetadataHolderService that androidx.car.app:app-projected merges into the manifest. Verified by merged-manifest diff; the fixed build is confirmed listed on a real head unit. Stated at its proven strength: the artifact is the documented AA requirement regardless (NavigationManager / cluster APIs live in it); whether it alone caused the invisibility is not fully isolated, and upstream's interim builds may never have had the issue.

f9ead6e - the full car-side port (upstream ac487f78 + follow-ups c409ad21, 679119d1, a2510700, and the 381cfd49 recents reshape): car search, route preview with live-traffic alternates, active turn-by-turn with maneuver icons + lane guidance, cluster trip updates, the real styled Vela map via MapSnapshotter (one shared renderer per session), MapFonts Roboto glyphs app-wide, and the unified recents timeline. The fork's D-pad rings preserved through every merge point.

Fork additions (none of this exists upstream)

Voice (final design after two review rounds + head-unit iteration): the engine preference is law.

  • Vela pinned (or Auto with the model installed): mic buttons on the home and nav screens (and the search screen, beside the host's field mic) record through CarAudioRecord into the same on-device Silero VAD + Whisper pipeline as the phone, via a PcmSource seam in WhisperRecognizer.listen() (phone path byte-identical). A silent car mic (common on aftermarket units) retries once on the phone's mic - still on-device. Failures toast and stop; speech is never rerouted against the pin. The recognizer pre-warms at session start.
  • System pinned: Vela draws no mic button - the platform forbids apps from starting the host voice session, and a mic that cannot listen is a dead button. "Hey Google" / the wheel button is the system path and routes into Vela via the NAVIGATE intents. (The mic inside the keyboard's search field is AA's own chrome, unremovable, always system.)
  • Voice commands (core/voice/CarCommands, unit-tested) dispatch via VoiceCommandRouter at the calling screen, before any push: "navigate home"/"find my car" land on their previews, "mute"/"unmute" act in place, "end navigation" ends the drive; anything else goes straight to results. Hardened: VAD window accumulator for partial car-mic reads, wall-clock listen deadline, terminal-read handling, PCM carry byte.

Turn-by-turn (4689fd69, ac2b63f5):

  • Search along route: nav-screen search + mic, corridor-filtered results (the phone's RouteCorridor), a pick becomes a stop via NavSession.addStop - the drive replans and announces itself.
  • Stop manager: a map-strip action lists remaining stops; one tap removes and replans (NavSession.remainingStops()/removeStop(), sharing addStop's replan path).
  • Category shortcuts: Gas / Food / Coffee / Groceries / Parking / EV charging as Google-style colored tiles (IMAGE_TYPE_LARGE so hosts can't tint them to blobs); a tap goes STRAIGHT to results (spinner -> list), and the grid lives as the first landing-list row. Labels localized x15, queries stay the stable English literals (the phone chips' rule).
  • Faster-route offers as head-up Alerts (API 5+) with Go/Done; strip button kept for older hosts.
  • Driving alerts (Settings -> Navigation -> Driving alerts, both off by default): spoken heads-up within 400 m of a mapped license-plate camera (bundled DeFlock data, offline, once per camera per trip); speed badge turns red at 5%+2 km/h over the posted limit.

Around the drive (4689fd69):

  • Arrival card with one-tap Save parking spot (the phone's ParkingStore), Find my car landing row, live Home/Work drive-time subtitles, and resume straight into guidance when the car connects mid-drive.

Map (79a5fee4, ac2b63f5):

  • Real dark map at night - the phone dark theme's exact Liberty palette as a style-JSON transform (CarNightStyle, lockstep-commented with applyDark), replacing the darkening tint filter; structurally validated against the live style (every rule hits: 10/10 ids, 26 road/casing layers, 25 label layers). Tint remains the cold-cache fallback.
  • Live traffic on the browse map when the phone's Traffic toggle is on - same keyless tiles, same 0.6 opacity, same below-the-labels placement.
  • Place details pane (rating, address, category, phone) behind a Details action on the route preview; host-gated while driving, full when parked.

Head-unit polish (driven by live testing on a real unit): search opens keyboard-down (keyboard on bar tap only), the map drops the on-frame attribution stamp (the ODbL credit is permanently anchored in Settings -> About, the placement OSM's guidance allows for driver displays), and the strip/icon issues found on-device (host icon-cache staleness, template strip slot behavior, the session-create API-level race) are each fixed at their root.

Free-text NAVIGATE geocoding (54a910f4): "navigate to central park" (geo:0,0?q=...) geocodes through Vela's own search into the route preview - upstream just opens the app. Coordinate parsing tightened to matchEntire so "Store 12, 34th St" is a query, not lat 12 lng 34.

Verification

Host gates green on every commit: both flavors compile + assemble, :core tests (incl. the new CarCommandsTest), detekt, static D-pad audit 0 violations, translations lock (554 keys x 14 locales).

Phone-side surfaces this PR touches, on device (map + search + settings phases): all legs clean so far - search 4/4 and settings 15/15 on every completed leg (four standard geometries; restricted + touch legs in flight; final grids land here when the sweep completes). One pre-existing harness bug fell out: search-history-astype missed on every leg on main too (A/B-confirmed) - the "Coffee" chip records its capital-C query literal while the check wanted exact-lowercase "coffee", and a single post-sleep dump raced the IME. Fixed with polling case-insensitive matchers (on_screen_ci_within), negative + positive controls run on device.

astype fixed (240x320) Driving alerts rows (240x320) Driving alerts rows (480x854)
astype nav240 nav480

Car-side: a DHU (Desktop Head Unit) gate is being brought up - the test flip turns out to carry full GMS + Gearhead, so projection screenshots of the car screens are possible locally after all; they will land here. Until then the car screens are host-gate + code-verified, and real-drive testing runs on the reporting head unit. The car mic path (CarAudioRecord) stays a real-head-unit test either way.

All ported car-side design and code by upstream PimpinPumpkin/Vela, credited per commit; everything under "Fork additions" is original to this fork.

On the head unit

Home: Search + Vela mic Categories Vela voice listening (on-device)
home cats listening
Search surface Search: Vela strip mic + the host's in-field system mic In-drive search: categories under the bar
search localmic indrive

Voice honors the engine preference end to end: LOCAL runs on-device Whisper (car mic, phone-mic retry when the car mic is dead); SYSTEM draws no in-app mic - Hey Google / the wheel button is the system path and routes into Vela via NAVIGATE intents. The in-field keyboard mic is Android Auto's own chrome and always uses the system recognizer; every mic Vela draws obeys the pin.

… list Vela

Vela never appeared in Android Auto's launcher, even with unknown sources
enabled (device report, OnePlus 12). The manifest declarations were all
correct - CarAppService with the NAVIGATION category, the automotive_app_desc
template capability, minCarApiLevel - but the build shipped only the base
androidx.car.app:app template library. Android Auto discovers a car app
through the CarAppMetadataHolderService that androidx.car.app:app-projected
merges into the manifest (the official docs' Android Auto artifact); without
it the host has nothing to hand-shake with and the app is simply never
listed. Verified by diffing the merged manifests: the fixed APK carries
androidx.car.app.CarAppMetadataHolderService, the old one does not.

The first-cut Android Auto port predates any real head-unit test (AGENTS
notes it untested), which is how this shipped invisible.
…o glyphs, recents timeline

Port of upstream's complete Android Auto stack (PimpinPumpkin/Vela ac487f78,
with follow-ups c409ad21, 679119d1, a2510700, and the 381cfd49 recents
reshape it depends on). Replaces the first-cut display-only car screen.

What the head unit gains: car-side search, route preview with alternates,
and active turn-by-turn with maneuver icons and lane guidance
(ManeuverMapper, LaneImage); assistant and geo: NAVIGATE intents (with
upstream's opaque-URI crash fix); trip updates to the instrument cluster.
The map is the real styled Vela basemap drawn via MapLibre's MapSnapshotter
onto the template surface - heading-up look-ahead camera in nav, speed-
tightened zoom, traffic-coloured route, speed badge, and a smoothing ticker
between fixes. One shared renderer per session (a swapped-in second instance
never receives the surface - upstream's hard-won note, kept).

Dependencies ported properly rather than adapted around: MapFonts (Roboto
glyphs over Liberty, app-wide - phone map and car snapshotter; probe-guarded
with stale-cache eviction; glyph host defaults to upstream's Pages until the
release is mirrored here - see ROADMAP) and the recents timeline reshape
(typed RecentPlace/RecentQuery entries, one chronological list in the search
overlay and the car main screen; the fork's D-pad rings and the clear-recents
ring box are kept through the merge, and the saved-row overflow icon adopts
upstream's dark-readable tint).

Strings: upstream's ten car strings plus the renamed clear-recents in their
ten locales; Hebrew, Japanese, and both Chinese variants authored here.

Verification: both flavors compile, :core tests green, static audit 0
violations, lint and translation gates green. No device gate exists for AA
(the golden flip phone cannot project) - real-car verification on the
reporting head unit; a DHU harness phase is on ROADMAP.

All car-side design and code by upstream PimpinPumpkin/Vela; this fork's
adaptations are the Hilt/CarDeps wiring already shared, locale additions,
and the D-pad ring preservation in the shared phone UI.
The fix stands on the docs requirement and the merged-manifest diff; the
invisibility causality is not fully isolated (the Android Auto app-list
rescan happened in the same step as the new install), and upstream's interim
builds may never have had the issue.
Two features upstream's Android Auto stack does not have:

Voice search on the car. WhisperRecognizer.listen() gains a PcmSource seam
(start/read/stop over 16 kHz mono PCM16) with the phone AudioRecord as the
default - byte-identical behavior when no source is passed. CarVoiceSearch
bridges CarAudioRecord (Car API 5+, records at the same 16 kHz Whisper
expects) into that seam, so the mic action on SearchCarScreen runs the same
on-device Silero VAD + Whisper pipeline as the phone mic; speech never
leaves the device. Gates: host carAppApiLevel >= 5, the Settings voice
toggle, an installed model; RECORD_AUDIO via carContext.requestPermissions.
Tap again to stop early (the phone's "done"); the transcript echoes into
the search field and runs as a normal search. NoSpeech gets a toast
(car_voice_nothing, all 15 locales) - phone-style silence after a car
spinner would read as broken.

Free-text NAVIGATE destinations. "Navigate to central park" style
geo:0,0?q=... intents used to return null from parseNavDest and just open
the app; now parseNavQuery + resolveFreeText geocode the text through the
same MapDataSource.search the search screens use, biased to the last known
location, and land in the route preview for the top hit (a miss toasts
mapvm_no_results). Coordinate parsing tightened from find() to
matchEntire(): "Store 12, 34th St" contains a coord-shaped substring but
is a query, not a destination at lat 12 lng 34.

Docs: ANDROID-AUTO.md gains a "What works in the car" section; FEATURES,
SPEC and AGENTS record both additions as fork-original (not ported).

Host gates: both flavors compile, :core tests, detekt, audit_static,
translations lock (539 keys x 15 locales). No device gate for AA exists
(the flip phone cannot project); real-car verification on the head unit.
The mic was only inside SearchCarScreen, so voice search took a tap on
Search first. The landing screen's action strip now carries the mic next
to Search (same gates: Car API 5+, voice toggle, model installed); the
transcript opens SearchCarScreen with the query already searched
(initialQuery). The tap flow (permission prompt, listening flag,
stop-early second tap, no-speech toast) moves into
CarVoiceSearch.micAction so both screens share one implementation.
Turn-by-turn, the priority:
- Search along route: the nav screen gains a search action and the mic;
  SearchCarScreen(alongRoute=true) fetches around the route midpoint and
  corridor-filters via RouteCorridor (the phone's own filter), and tapping
  a result calls NavSession.addStop - the drive replans through it and
  announces itself. CategoriesCarScreen (GridTemplate) offers the phone's
  four along-route categories (Gas/Food/Coffee/Groceries; labels localize,
  queries stay the stable English literals) from the landing screen and
  the same add-stop flow in-drive.
- Voice commands (core/voice/CarCommands, unit-tested): navigate home /
  take me to work / find my car / mute / unmute / end navigation; every
  unmatched phrase is a search with the leading nav verb stripped, so the
  grammar can only upgrade a transcript, never lose one. Wired into the
  landing and nav mics.
- Faster-route offers pop as a head-up Alert (Car API 5+, Go/Done, one per
  candidate); the strip button stays for older hosts.
- Driving alerts (Settings -> Navigation -> Driving alerts, both off by
  default): a spoken heads-up within 400 m of a mapped license-plate
  camera (bundled FlockCameras corridor scan - offline, once per camera
  per trip), and the car speed badge turning red at 5% + 2 km/h over the
  posted limit.

Around the drive:
- Arrival card (ArrivalCarScreen): one-tap Save parking spot into the
  same ParkingStore the phone's Park feature reads; ends the session
  cleanly like the phone's arrival dismiss.
- Landing screen: Find my car row when a spot is saved (routes back to
  it), live Home/Work drive-time subtitles (one directions pair per
  screen), and the category grid action.
- Resume: a session created while nav is already running lands straight
  in guidance (guarded on !arrived so a stale arrived state can't bounce
  screens).

Harness (pre-existing bug, A/B-confirmed on main): search-history-astype
missed on every leg because run_coffee's chip path records its stable
English query literal "Coffee" while the check wanted exact-lowercase
"coffee" - and the old single post-sleep dump also raced the IME.
find_text_ci/on_screen_ci(_within) added (case-insensitive exact,
polling); negative + positive controls run on device.

Host gates: both flavors compile + assemble, CarCommandsTest + :core
green, detekt, audit_static 0, translations lock re-recorded (547 keys x
14 locales). Device verification (search + settings phases, all legs)
running; goldens land with it.
The car's night map was the day snapshot run through a darkening color
filter - readable, but muddy, and the header comment admitted a real dark
vector style was the follow-up. This is it: CarNightStyle.darken applies
the phone dark theme's exact Liberty recolouring (applyDark in
VelaMapView) as a style-JSON transform, since the MapSnapshotter loads a
style once and exposes no live Style to mutate. applyDark carries a
lockstep note pointing here.

Structurally validated against the live Liberty style: every rule hits -
10/10 exact layer ids, all 26 road/casing layers, 25 symbol layers
retinted, 9 non-green landuse fills.

Graceful ladder in the renderer: night + style JSON in hand -> the dark
transform (no tint); night with a cold MapFonts cache or a failed parse
-> the old tint filter, so the worst case is exactly the previous
behaviour; day -> untouched. A mid-session day/night flip rebuilds the
snapshotter (the reuse check now keys on the night state too).
No deferrals: the remaining buildable items from the enhancement list.

- In-drive stop manager (StopsCarScreen, via a new map-strip action shown
  while the trip has stops): lists the remaining stops in travel order,
  one tap removes and replans. Core grows NavSession.remainingStops() and
  removeStop() - addStop's replan extracted into a shared replanVia() so
  add and remove ride identical user-ordered reroute semantics.
- Place details (PlaceDetailsCarScreen, PaneTemplate): rating, address,
  category, phone from the search result, behind a Details action on the
  route preview (search results now pass their full Place through). The
  host content-gates it while driving and shows it all when parked.
- Live traffic on the car browse map: CarNightStyle.withTraffic injects
  the phone's exact keyless traffic raster (same tiles, same 0.6 opacity,
  same below-first-symbol placement as ensureTraffic) into the snapshot
  style when the phone's Traffic toggle is on. Composes with the night
  transform; the snapshotter reuse key now includes the traffic state.
- Categories grow Parking and EV charging (car-first; the phone chips
  keep their four), same label-localizes/query-stays-English rule.

Gates: both flavors compile, :core tests, detekt, audit_static 0,
translations lock re-recorded (554 keys x 14 locales).
@alltechdev alltechdev changed the title Android Auto: full car-side navigation (port of upstream's AA stack) Android Auto: full car-side navigation + fork feature suite Jul 23, 2026
…clean

4 standard geometries (search 4/4, settings 15/15 each), restricted
kyocera + sonim (4/4, 14/14 - place-pages n/a), touch settings 15/15.
Zero missed. Includes the astype frame the case-insensitive fix restored
and the Navigation spoke's new Driving alerts rows.
@alltechdev

Copy link
Copy Markdown
Owner Author

Device sweep complete, all 13 leg-phases clean, zero missed:

Leg search settings
kyocera-e4810 240x320@160 4/4 15/15
sonim-x320 480x854@320 4/4 15/15
kyocera-duraxe-e4830 240x320@120 4/4 15/15
sonim-x320-225 480x854@225 4/4 15/15
kyocera-e4810 restricted 4/4 14/14 (place-pages n/a)
sonim-x320 restricted 4/4 14/14 (place-pages n/a)
kyocera-e4810 touch - 15/15

Goldens committed in 12a7cab - including the search-history-astype frame the case-insensitive harness fix restored (it was missing on main too) and the Navigation spoke's new Driving alerts rows at both geometries. Frames eyeballed. DHU projection screenshots are the remaining verification item.

@alltechdev

Copy link
Copy Markdown
Owner Author

Dynamic focus walk of the Navigation spoke (the two new Driving alerts rows), PR build, 240x320@160: PASS, no focus-integrity failures - every row takes focus in order with the orange ring on each stop. Scoped to the one changed spoke on one leg per the verification bar; a ToggleRow's ring behavior is geometry-independent.

Conflicts: goldens resolved by provenance (branch frames for search/
settings - Roboto + Driving alerts rows; main's flat-black frames for the
Black-theme 55/57-60 set), gradle kept the branch's app-testing line on
top of the shared app-projected, FEATURES unioned with main's post-rename
theme wording. Both flavors compile, :core green, translations lock OK.
- Resume push: dispatched on plain Main (not the scope's Main.immediate)
  so it runs after the library roots the landing screen - guidance was
  landing UNDER it and the feature silently no-oped.
- Terminal states peel to a marker: popTo(active-nav) then pop, so
  arrival under a covering screen (along-route search / stops /
  categories) can no longer strand a dead guidance screen.
- Camera alerts: the route key is only recorded once FlockCameras is
  loaded - a GPS fix beating the dataset parse permanently cached an
  empty camera list for the trip.
- listen(): a negative read is terminal (CarAudioRecord returns -1
  forever after a host remote-close; the loop spun at 100% CPU holding
  audio focus); zero-byte reads back off 10 ms instead of hot-looping.
- geo: parsing tolerates RFC 5870 extras: optional altitude component
  (non-capturing) and ;u=/;crs= params stripped - matchEntire had been
  swallowing those NAVIGATE intents whole.
- CarCommands: bare 'drive'/'take me' prefixes removed ('drive in movie
  theater' was searching 'in movie theater'); verb-only home phrasings
  covered as whole phrases; tests pin the non-stripping.
- Renderer: a mid-session day/night or traffic flip rebuilds the
  snapshotter from the stored surface container - sunset no longer
  leaves the map in the day palette.
- In-drive 'find my car' routes to the saved spot like the landing
  handler (it was corridor-searching the literal transcript).
- Car PCM source carries an odd trailing byte into the next read
  instead of dropping it (a single odd read desynced the whole capture).
- astype check requires the typed field text before the ci-match, so a
  failed overlay open can't false-pass on the bare map's Coffee chip.

Gates: both flavors compile, CarCommandsTest + :core green, detekt,
harness syntax.
…sults

Head-unit report, three fixes:
- The landing template renders only TWO strip actions on real hosts, so
  the categories action silently pushed the MIC off. The strip is back
  to mic + Search; Categories becomes a Google Maps-style list row with
  a colored tile (first row, browsable), and the row cap accounts for it.
- Category tiles are now colored circles with white glyphs, rendered
  with IMAGE_TYPE_LARGE - IMAGE_TYPE_ICON let the host tint them into
  the grey/broken blobs on the report (and two glyph paths were bad;
  all six re-drawn: amber gas, orange food, brown coffee, green
  groceries, blue parking, teal EV bolt). Parked-car row gets its blue
  tile too.
- Tapping a category goes STRAIGHT TO RESULTS (CategoryResultsCarScreen:
  loading -> list, tap previews or adds a stop, corridor-filtered
  in-drive) instead of opening the search screen with the query typed
  into the box.
The snapshotter's protected addOverlay hook (which stamps the logo and
the OpenFreeMap / OpenMapTiles / OpenStreetMap line onto every frame) is
overridden to a no-op on the car surface (user request). To stay inside
OSM's attribution guidance for space-constrained displays, About gains a
permanent 'Map data (c) OpenStreetMap contributors' line
(translatable=false - proper names).
Across three head-unit builds the host dropped the FIRST strip action
whenever it ran short of slots (mic+search showed both once, then every
later build dropped the mic from the head). Search now leads and the mic
takes the tail slot the host renders most reliably.
The landing screen templates ONCE at session create, before the host
handshake reports carAppApiLevel - so available()'s API-level gate
failed exactly there and the mic vanished from the home screen while
every later-built screen kept it (head-unit evidence). Visibility now
gates on the user toggle + installed model only; the API level is
checked at tap time, where an ancient host gets a graceful toast
instead of a hidden mic.

SearchTemplate no longer pops the keyboard by default - it opens on a
bar tap (user request).
Voice transcripts now land straight on the results screen (searching
spinner -> list) from the landing and nav mics instead of a prefilled
search box (user request).

The landing mic action stopped rendering across builds while its Action
structure never changed - the junk glyphs on the categories grid (info
circle, compass, nav arrow: random stand-ins, not our vectors) point at
the host caching app icons by resource id across APK installs and
serving stale entries after ids shuffled. The mic drawable is renamed
(ic_car_mic2) so its id maps to a cache key the host has never seen;
clearing Android Auto's cache on the phone is the belt-and-braces step
if a unit still shows junk.
The search screen carried TWO mics (the host's inline SearchTemplate
voice input plus our strip action) and our separate CarAudioRecord
capture flow was dead on the reporting head unit. All mic entry points
(landing strip, nav strip) now open the search surface whose inline
host mic is the one that demonstrably works; our duplicate strip mic is
gone. Voice commands survive the rewire: onSearchSubmitted parses
through CarCommands, so a spoken 'navigate home' / 'mute' / 'find my
car' acts instead of searching those words, and plain transcripts fall
through to results as before. The on-device CarVoiceSearch capture path
stays in the tree for a future head-unit-verified return.
The shared-mic rewire left unused CarVoiceSearch fields and handleVoice/
previewShortcut handlers on the landing and nav screens - the search
surface's CarCommands-on-submit now owns all of that. Detekt clean.
The car mic now branches on VoiceSearch.resolvedMode exactly like the
phone mic (user requirement):
- LOCAL (Vela on-device pinned, or Auto with the model installed): the
  car mic records through CarAudioRecord into the on-device Whisper
  pipeline; the transcript lands on the search surface where commands
  parse and results load. A capture failure toasts and STOPS - speech is
  never silently rerouted to Google against a Vela pin.
- SYSTEM (Google/provider pinned, or Auto without the model): the mic
  opens the search surface directly; its inline host mic IS the system
  recognizer.
- NONE hides the mic, same as the phone.

SearchCarScreen's submit handling (voice commands + search) moved to a
class-level handleSubmitted shared by typed submits and mic transcripts,
so 'navigate home' spoken through EITHER engine acts instead of
searching those words. The landing mic tap previously pushed the search
screen unconditionally - the tap path is rebuilt with the mode branch,
which also replaces the flow behind the reported bounce-to-home tap.
When the resolved voice mode is LOCAL, the car session warms the Whisper
recognizer at create (same as the phone's app-start warm-up), so the
first mic tap listens immediately instead of loading the model mid-tap.
The Listening toast goes LONG - SHORT died ~2 s into a capture that can
legitimately run 15 s.
CarAudioRecord can 'succeed' while delivering silence on setups with no
usable car microphone (head-unit report: 'didn't catch that, not hearing
me'). When the car-mic capture yields no transcript and the user hasn't
cancelled, the tap retries ONCE on the phone's own microphone - the same
on-device Whisper pipeline, the same user preference, just the mic that
demonstrably exists. A second Listening toast marks the retry; a genuine
no-speech still ends in the honest toast.
Template apps cannot start the host's voice input programmatically (the
platform reserves it for Hey Google / the wheel button), so a
Google-pinned mic tap cannot be one-tap-to-listening. The closest
possible: arriving via a mic button now opens the search surface with
the keyboard (and its voice key) already up (voiceEntry flag); a plain
Search tap keeps the keyboard down per the earlier request.
Commands dispatch at the CALLER (new VoiceCommandRouter), never from a
pushed screen's init: 'end navigation' ends the drive again (EndNav ->
stopNav from the nav mic; navSession.stop + popToRoot for typed submits),
'navigate home'/'find my car' land on their preview unburied, mute/
unmute act in place with a confirming toast, and no-spot find-my-car
gets its honest toast back. Plain speech goes straight to results.

The LOCAL pin holds on every surface: the search screen re-gains the
Vela strip mic in LOCAL mode (the host's in-field mic is the system
recognizer and cannot be removed - correct for SYSTEM users, a silent
reroute for Vela-pinned ones).

The phone-mic fallback fires only when the car mic is DEAD (failure, or
no-speech with ~zero peak level) - a real said-nothing into a working
car mic no longer starts a second capture. available() caches ~5 s (it
walked PackageManager on every nav tick). listen() gains a VAD
accumulator (partial pipe reads starved Silero) and a wall-clock
deadline (a zero-yield source held audio focus forever).

Docs updated to the final voice design (ANDROID-AUTO.md, AGENTS.md).
Two truths made structural (user: no nonsense):
- The in-field mic on the search keyboard is Android Auto's own chrome:
  no app can remove it and it always uses the system recognizer. Every
  mic VELA draws honors the engine pin; in LOCAL mode the Vela strip mic
  on the search screen is the on-device path beside it.
- In SYSTEM mode Vela draws NO mic button: the platform forbids apps
  from starting the host voice session, so the old button could only
  open a keyboard - a dead mic. System voice on AA is Hey Google / the
  wheel button, which routes into Vela via NAVIGATE intents. The
  voiceEntry keyboard hack and the onSystem plumbing are deleted;
  available() is LOCAL-only. Search opens keyboard-down, always.
Docs updated in the same commit.
Home screen with Search + the Vela mic on the strip, the colored
category grid, the search surface (Vela strip mic beside the host's
in-field system mic), and Vela's on-device voice mid-listen.
Google Maps-style (user request): opening search while navigating shows
the six colored category rows before you type - each goes straight to
corridor-filtered results whose pick becomes a stop. Exactly six rows,
the template's cap; the browse-mode search keeps its plain hint.
The shared CarMapRenderer kept posting frames to the surface while a
non-map template (search with the category rows, lists, details) was
layered above it - the stale buffer posts bled through the host overlay
as blocky tearing (head-unit report). The three map screens now pause()
the renderer from onStop and start() (which unpauses and kicks a render)
from onStart, so frames flow only while a map screen is actually in the
foreground. The session-shared renderer design is unchanged - the
surface callback is never swapped, only production gates.
Car searches and car-started trips never recorded into the recents
stores - the landing screen's recents were all phone activity (user
caught it). Now: submitted searches and mic transcripts add to
RecentSearchStore (never per-keystroke suggestions), and starting a
route records the destination into RecentPlaceStore - the same stores
the phone's timeline and the car landing screen read, so history flows
both ways.
Two conflicts in WhisperRecognizer, both additive:
- companion object: keep the car mic's EMPTY_READ_SLEEP_MS alongside
  main's RAM-scaled REAP_IDLE_MS.
- listen(): keep main's lease-wrapper split (listen -> listenInner) and
  thread the car's PcmSource parameter through both, so the Android
  Auto mic keeps the identical VAD + decode pipeline while inheriting
  the lease/cancellation-safety and idle-reap semantics from main.
@alltechdev alltechdev changed the title Android Auto: full car-side navigation + fork feature suite Android Auto: full car-side navigation + fork feature suite [won't appear in cars without an allowlist workaround - see PimpinPumpkin/Vela#179] Jul 24, 2026
@alltechdev
alltechdev merged commit eedf492 into main Jul 24, 2026
1 check passed
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