Greek voice commands for Android Auto. Google Assistant and Gemini don't understand Greek in the car; Pesto does. Open it on the car screen, it listens immediately — say an address to start Google Maps navigation, or a contact name to place a call.
A personal tool with no backend and no analytics.
Why "Pesto". The app was called Mila (Μίλα) until v0.1.6. To open it by
voice you say its name to Google Assistant or Gemini, and they listen in
English. Spoken with a Greek accent, "Mila" came back as "Myla", "Miele" and
"my", and the app didn't open. Pesto is a word English speech recognition
already knows and it sounds the same in Greek. The package name is still
dev.altany.mila: Play identifies an app by it, so changing it would publish
a separate app instead of an update. Internal class, keystore and secret names
keep the old name too; nobody sees them.
It has to be installed from the Play Store to work in a car. Android Auto does not show sideloaded car apps on a real head unit — it never even looks at them — and the "Unknown sources" developer setting doesn't change that. A sideloaded build works fine on the desktop emulator, which is what makes this so easy to get wrong. A private internal testing track is enough; a public listing isn't needed. See Installing.
Open Pesto on the car screen. It starts listening immediately (a short tone tells you the mic is open) — no mic tap.
- Πλοήγηση (Navigate) — say a destination. Pesto hands it to Google Maps and navigation starts.
- Κλήση (Call) — say a contact name. If one contact clearly wins, it dials. If several are close, it shows a short pick list instead of guessing.
You can also just say what you want and skip the buttons: an opening verb settles it either way, so "κάλεσε τον Δημήτρη" dials even when Navigate is selected, and "πήγαινε στην Πάτρα" navigates. Recognised verbs are κάλεσε, πάρε, τηλεφώνησε for calls and πήγαινε, πλοήγηση, οδήγησε, πάμε for navigation; the verb and its article are stripped before matching.
Two big buttons switch modes, and switching restarts listening. If the recognizer gives up early, the screen keeps whatever it heard and offers Ξαναπές το (retry) or using the partial text.
The steering-wheel button opens the phone's assistant, which can launch apps by name — but it listens in English, not Greek. That's why the app is called Pesto: English recognition already knows the word, and it sounds the same said with a Greek accent.
Say "Launch Pesto". The verb does the work. Plain "Pesto" gets you a reply about the sauce, or that the assistant can't open apps. "Open the Pesto app" works too, but it's longer and the article is easy to drop.
- Android Auto projection app built on
androidx.car.app(the Android for Cars App Library), categoryPOIso it appears on the Android Auto launcher. This is not an Android Automotive OS app — it runs on the phone and projects templates to the car screen. MilaCarAppService→MilaSession→ screens built from car app templates.- Speech recognition runs on the phone via Android's
SpeechRecognizerwith localeel-GR(see reasoning below). - Navigate mode hands the recognized text to Google Maps with a
google.navigation:q=intent (fallbackgeo:0,0?q=) viaCarContext.startCarApp, so Maps takes over the car screen. - Call mode matches the recognized name against phone contacts —
accent/case-insensitive, Greeklish-aware via a Greek→Latin transliteration
layer, fuzzy with a confidence threshold — then places the call with
ACTION_CALL. - Runtime permissions can't be granted on the car screen, so a one-time
phone-side
SetupActivityrequests mic, contacts and call permissions. The car screen detects missing permissions and says to finish setup on the phone.
Greek speech has to match contacts saved in Greek, in Greeklish, or in Latin.
Both the spoken name and each contact name are reduced to a canonical Latin
"phonetic key" (GreekText.canonicalKey): accents and case are stripped, and
Greek digraphs collapse to sounds (ου→u, μπ→b, αι→e, ει/οι/η/υ→i).
Some Greeklish letters are genuinely ambiguous, so they expand into a small set of variant keys and the best-scoring reading wins:
| Letter | Readings | Example |
|---|---|---|
x |
χ or ξ | Xristos (Χρήστος) / Xenia (Ξένια) |
h |
χ or η | Hristos / Xrhstos |
b |
μπ or β | Babis (Μπάμπης) / Basilis (Βασίλης) |
u |
ου or υ | Loukas (Λουκάς) / Kuriakos (Κυριάκος) |
ai ei oi |
digraph or two vowels | Kaiti (Καίτη) / Mixail (Μιχαήλ) |
Scoring is Levenshtein similarity, computed both over the whole name and token by token, so saying just "Ελένη" matches "Ελένη Βασιλείου". A candidate must clear a minimum score to be offered at all, and must beat the runner-up by a margin to be dialled without asking — otherwise the pick list appears.
Matching happens in memory, not in a SQL LIKE query, because the contacts
provider can't compare Greek speech to Greeklish spellings. Contacts are read
off the main thread and cached when the screen opens.
CarAudioRecord (car app library, API level 5+) records raw PCM from the car
microphone — it does no speech-to-text. Turning that audio into Greek text
would require either a cloud STT API (backend, cost, privacy) or a bundled
on-device model (size, complexity). Android's SpeechRecognizer does full
Greek recognition through the phone's Google recognition service, and the
proven open-source reference for this approach in real cars is
aa-speech-to-text (GPL-3.0),
which this project used as a behavior reference.
Note that this means Pesto listens through the phone's microphone, not the car's. Android Auto's own Assistant uses the car microphone, so it still hears you with the phone in a bag; Pesto will not.
Greek needs a data connection. Google ships no on-device (offline) speech
model for el-GR — the phone's own language-pack list contains no Greek at
all, and the recognizer reports Failed to get language pack of required locale and falls back to the network recognizer. There is no setting to
change this; it applies to any app using Android's SpeechRecognizer. Expect
recognition to fail without signal, which is what the retry action is for.
The recognizer decides for itself when speech has ended and treats the
EXTRA_SPEECH_INPUT_* timeouts as hints. A car cabin is never silent, so left
alone it holds the microphone until its own 60-second server cap — the driver
speaks and nothing happens for a minute, with separate attempts accumulating
into one transcript. Pesto therefore does its own endpointing: once words start
arriving, a pause with no new partial results ends the turn, and a hard ceiling
backstops it. Erring the other way is just as bad: too short a timeout cuts the
driver off mid-address.
Greek command verbs are easy to mishear — the recognizer has returned
"θάλασσα" for "κάλεσε", which quietly turned a phone call into directions to a
beach bar. Three hypotheses are requested and all of them are used: the intent
comes from the first alternative that opens with a real command verb, and
contact matching scores every alternative and keeps each contact's best, so a
name mangled in the top transcription can still be found in another reading.
Debug builds log the hypotheses under the Pesto tag.
Nothing here is Greek by necessity — the problem is that Android Auto supports a fixed list of languages and yours may not be on it. Forking this for another language means changing five things:
- The recognition locale.
SpeechController.RECOGNITION_LOCALEis"el-GR". Set it to yours, and check Android's recognizer actually supports it — the list is not the same as the one Android Auto supports, which is the whole reason this app exists. - The command words.
SpokenCommandholdscallVerbs,navVerbsandfillers— the words that mean "call", "navigate", and the articles that get stripped before matching. Replace them with your language's. Note the words are normalized before comparison, so write them the wayGreekText.normalizewould leave them. - The name matching.
GreekTextmaps Greek to a Latin "phonetic key" so that a name spoken in Greek matches a contact saved in Latin letters. If your language already uses the Latin alphabet, you probably don't need any of this: strip accents and case, and compare withsimilaritydirectly. If it uses another script, you need the equivalent mapping — and the interesting part is the ambiguity handling, where one letter has several plausible readings and each expands into variants. - The interface text.
app/src/main/res/values/strings.xml. - The launcher name.
app_namein the same file. Pick something English speech recognition already knows as a word — the phone's assistant listens in English whatever language the app works in, and a transliterated name in your own language will get misheard. That's the whole reason this one is called Pesto and not Mila.
The parts worth keeping as-is are the ones that took the longest to get right and aren't language-specific: ending the listening turn yourself rather than trusting the recognizer, and using every recognition hypothesis instead of the top one.
One thing that isn't optional: the app has to be installed from the Play Store to appear in a real car. A sideloaded build works on the emulator and is invisible in the car. An internal testing track is enough — see below.
Requirements: JDK 17+, Android SDK (platform 36). A local.properties with
sdk.dir pointing at the SDK (Android Studio creates this automatically).
./gradlew assembleDebug # debug APK
./gradlew test # unit tests (transliteration + matching)
./scripts/build-release.sh # signed release APK (creates keystore on first run)adb install -r app/build/outputs/apk/release/app-release.apkOr download the APK from the GitHub Releases page in the phone's browser and open it (allow "install unknown apps" for the browser when prompted).
Sideloaded car apps are hidden until developer mode is on:
- Open the Android Auto app on the phone (in phone Settings → search "Android Auto").
- Tap the version number ~10 times to enable Developer settings.
- Overflow menu → Developer settings → enable Unknown sources.
- Restart Android Auto (or replug the phone). Pesto appears in the car launcher.
-
Install the DHU: Android Studio → SDK Manager → SDK Tools → Android Auto Desktop Head Unit Emulator. From the command line instead:
~/Library/Android/sdk/cmdline-tools/latest/bin/sdkmanager "extras;google;auto"
On first run you may need
chmod +xon thedesktop-head-unitbinary. -
On the phone: Android Auto Developer settings → Start head unit server.
-
Connect the phone by USB and forward the port, then run the DHU:
adb forward tcp:5277 tcp:5277
~/Library/Android/sdk/extras/google/auto/desktop-head-unitThe car screen appears in a desktop window with the phone providing the apps.
Stop the head unit server before driving. Android Auto projects one session at a time, so a server left running keeps the phone busy serving the DHU and the app can be missing from a real car's launcher. Use Android Auto's overflow menu → Διακοπή διακ. κεντρ. μονάδας (stop head unit server) before plugging into the car. Take care not to hit Παύση λειτ. προγραμματιστή (pause developer mode) next to it — that hides every sideloaded app with no other symptom.
GitHub Actions (build.yml):
- Push to
main→ tests run and a debug APK is uploaded as a workflow artifact. - Tag
v*→ tests run, then the signed APK and app bundle are attached to a GitHub Release and the bundle is published to Play's internal testing track. The build reaches the phone as an ordinary app update.
Release in one step:
scripts/release.sh # patch bump
scripts/release.sh 0.2.0 # explicit versionThat means a change can go from an edit to running in the car without a laptop: push a tag from anywhere, and Play delivers it.
The app must be installed from Play to appear in a real car, so the release job publishes automatically. Two small scripts do it:
scripts/play-token.shswaps the service account key for a short-lived token using the standard JWT-bearer flow. Done directly rather than through a generic auth action, which mints tokens by impersonating the service account and needs the IAM Credentials API plus a token-creator role granted to the account on itself.scripts/publish-play.shmakes four REST calls — open an edit, upload the bundle, point the internal track at it, commit — retrying on 5xx, because the Play API returns 503 often enough that one attempt isn't a fair test.
No third-party action handles the credential. Setting this up needs a Google
Cloud service account with the Google Play Android Developer API enabled,
invited into Play Console under Users and permissions with only Release apps
to testing tracks and View app information. Its JSON key goes in the
PLAY_SERVICE_ACCOUNT_JSON secret. Scoped that way, the worst a leaked key
could do is push a build to your own test track.
Local and CI builds sign with the same key, so new versions install over
old ones. scripts/make-keystore.sh generates keystore/mila.jks +
keystore.properties once (both gitignored). CI gets the same key via these
GitHub Actions secrets:
| Secret | Value |
|---|---|
MILA_KEYSTORE_BASE64 |
base64 -i keystore/mila.jks |
MILA_KEYSTORE_PASSWORD |
storePassword from keystore.properties |
MILA_KEY_ALIAS |
mila |
MILA_KEY_PASSWORD |
keyPassword from keystore.properties |
GPL-3.0 — see LICENSE. Behavior informed by aa-speech-to-text by ron.gr (GPL-3.0); no code was copied.