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
Conversation
… 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).
…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.
Owner
Author
|
Device sweep complete, all 13 leg-phases clean, zero missed:
Goldens committed in 12a7cab - including the |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Important
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:applibrary, and the AA host discovers car apps through theCarAppMetadataHolderServicethatandroidx.car.app:app-projectedmerges 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-upsc409ad21,679119d1,a2510700, and the381cfd49recents 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.
CarAudioRecordinto the same on-device Silero VAD + Whisper pipeline as the phone, via aPcmSourceseam inWhisperRecognizer.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.core/voice/CarCommands, unit-tested) dispatch viaVoiceCommandRouterat 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):RouteCorridor), a pick becomes a stop viaNavSession.addStop- the drive replans and announces itself.NavSession.remainingStops()/removeStop(), sharingaddStop's replan path).Around the drive (
4689fd69):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):CarNightStyle, lockstep-commented withapplyDark), 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.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 tomatchEntireso "Store 12, 34th St" is a query, not lat 12 lng 34.Verification
Host gates green on every commit: both flavors compile + assemble,
:coretests (incl. the newCarCommandsTest), 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-astypemissed 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.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
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.