A Rust implementation of the Android Auto protocol — both a reusable library and a working head unit application. Connect an Android phone to a Linux host and get a full Android Auto experience, including wireless connection via Bluetooth.
- What is this?
- How it works
- Features
- Prerequisites
- Building
- Running the head unit
- Using the library
- Architecture
- Contributing
- License
android-auto is two things in one repository:
- A library crate (
android-autoon crates.io) that implements the Android Auto wire protocol. You can embed it in your own Rust application to build a custom head unit. - A head unit example application (
examples/main/) that demonstrates the library in action, allowing a Linux machine to act as an Android Auto head unit for a connected phone.
Android Auto uses a binary framing protocol over a TLS-secured transport. The phone (acting as the server) and the head unit (acting as the client) exchange protobuf-encoded messages. At a high level:
- Transport layer — wired USB (via the
usbfeature) or wireless (Wi-Fi + Bluetooth for handoff, via thewirelessfeature). Bluetooth is used to advertise the head unit and negotiate the wireless connection; actual media data flows over Wi-Fi. - TLS handshake — the head unit authenticates using an X.509 client certificate. This library handles the full TLS negotiation via
rustls. - Frame layer — messages may be split across multiple frames (
FrameHeaderType). The library reassembles them transparently. - Channel layer — Android Auto multiplexes multiple logical channels over a single connection (video, audio, input, sensors, etc.). Each channel has its own message types, encoded with protobuf.
The library exposes this through a clean async trait interface so that integrators only need to handle the application-level events they care about.
- Wired USB Android Auto connections (enable with the
usbfeature) - Wireless (Bluetooth-initiated) Android Auto connections (enable with the
wirelessfeature) - Full TLS client support via
rustls - Protobuf message encoding/decoding for all standard Android Auto channels
- Async-first API built on
tokio - Configurable head unit identity (
HeadUnitInfo,AndroidAutoConfiguration) - Video channel trait (
AndroidAutoVideoChannelTrait) for custom video rendering backends - Audio output and input channel traits (
AndroidAutoAudioOutputTrait,AndroidAutoAudioInputTrait) - Input channel trait (
AndroidAutoInputChannelTrait) for touchscreen and keycode support - Sensor channel trait (
AndroidAutoSensorTrait) for reporting sensor data to the phone - Navigation channel trait (
AndroidAutoNavigationTrait) for receiving turn-by-turn updates
- Rust (stable, recent version recommended — see
rust-versioninCargo.toml) - Tokio async runtime (pulled in automatically as a dependency)
- For wired USB support: enable the
usbfeature (uses thenusbcrate) - For wireless support: enable the
wirelessfeature (uses thebluetooth-rustcrate) and ensure a system Bluetooth stack is accessible - A phone running Android with the Android Auto app installed
Clone the repository and build with Cargo:
git clone https://github.com/uglyoldbob/android-auto.git
cd android-auto
cargo build --releaseTo enable wired USB support:
cargo build --release --features usbTo enable wireless (Bluetooth-initiated) Android Auto support:
cargo build --release --features wirelessTo enable both:
cargo build --release --features usb,wirelessThe head unit is provided as an example application. Run it with:
cargo run --example main --releaseNote: the example uses several dev-dependencies (e.g.
eframe,openh264,cpal). Make sure all system libraries they require are installed.
Wired connection: plug your phone in via USB and build with the usb feature. The head unit will detect the connection and initiate the Android Auto handshake automatically.
Wireless connection: build with the wireless feature and ensure Bluetooth is enabled on both your host machine and phone. The library starts a Bluetooth server that advertises the head unit; the phone will discover it and negotiate a Wi-Fi session.
Add android-auto to your Cargo.toml:
[dependencies]
android-auto = "0.3.3"
# For wired (USB) support:
android-auto = { version = "0.3.3", features = ["usb"] }
# For wireless (Bluetooth-initiated) support:
android-auto = { version = "0.3.3", features = ["wireless"] }
# For both:
android-auto = { version = "0.3.3", features = ["usb", "wireless"] }Call android_auto::setup() once at program startup before doing anything else. It installs the TLS crypto provider and returns an AndroidAutoSetup token that must be passed to run() (and related methods). Requiring this token at the call site is a compile-time guarantee that initialisation is never accidentally skipped:
fn main() {
let setup = android_auto::setup();
// pass `setup` to run() later …
}use android_auto::{
AndroidAutoConfiguration, AndroidAutoMainTrait, AndroidAutoVideoChannelTrait,
AndroidAutoAudioOutputTrait, AndroidAutoAudioInputTrait, AndroidAutoInputChannelTrait,
AndroidAutoSensorTrait, HeadUnitInfo, VideoConfiguration, InputConfiguration,
SensorInformation, AudioChannelType, SendableAndroidAutoMessage,
};
struct MyHeadUnit;
#[async_trait::async_trait]
impl AndroidAutoSensorTrait for MyHeadUnit {
fn get_supported_sensors(&self) -> &SensorInformation { todo!() }
async fn start_sensor(&self, _: android_auto::Wifi::sensor_type::Enum) -> Result<(), ()> { Ok(()) }
}
#[async_trait::async_trait]
impl AndroidAutoAudioOutputTrait for MyHeadUnit {
async fn open_output_channel(&self, _: AudioChannelType) -> Result<(), ()> { Ok(()) }
async fn close_output_channel(&self, _: AudioChannelType) -> Result<(), ()> { Ok(()) }
async fn receive_output_audio(&self, _: AudioChannelType, _: Vec<u8>) {}
async fn start_output_audio(&self, _: AudioChannelType) {}
async fn stop_output_audio(&self, _: AudioChannelType) {}
}
#[async_trait::async_trait]
impl AndroidAutoAudioInputTrait for MyHeadUnit {
async fn open_input_channel(&self) -> Result<(), ()> { Ok(()) }
async fn close_input_channel(&self) -> Result<(), ()> { Ok(()) }
async fn start_input_audio(&self) {}
async fn stop_input_audio(&self) {}
async fn audio_input_ack(&self, _: u8, _: android_auto::Wifi::AVMediaAckIndication) {}
}
#[async_trait::async_trait]
impl AndroidAutoInputChannelTrait for MyHeadUnit {
async fn binding_request(&self, _: u32) -> Result<(), ()> { Ok(()) }
fn retrieve_input_configuration(&self) -> &InputConfiguration { todo!() }
}
#[async_trait::async_trait]
impl AndroidAutoVideoChannelTrait for MyHeadUnit {
async fn receive_video(&self, _data: Vec<u8>, _timestamp: Option<u64>) {}
async fn setup_video(&self) -> Result<(), ()> { Ok(()) }
async fn teardown_video(&self) {}
async fn wait_for_focus(&self) {}
async fn set_focus(&self, _focus: bool) {}
fn retrieve_video_configuration(&self) -> &VideoConfiguration { todo!() }
}
#[async_trait::async_trait]
impl AndroidAutoMainTrait for MyHeadUnit {
async fn connect(&self) {}
async fn disconnect(&self) {}
async fn get_receiver(&self) -> Option<tokio::sync::mpsc::Receiver<SendableAndroidAutoMessage>> {
None
}
}
#[tokio::main]
async fn main() {
let setup = android_auto::setup();
let config = AndroidAutoConfiguration {
unit: HeadUnitInfo {
name: "My Head Unit".to_string(),
car_model: "My Car".to_string(),
car_year: "2024".to_string(),
car_serial: "000000".to_string(),
left_hand: true,
head_manufacturer: "ACME".to_string(),
head_model: "HU-1".to_string(),
sw_build: "1".to_string(),
sw_version: "1.0".to_string(),
native_media: false,
hide_clock: None,
},
custom_certificate: None,
};
let mut js = tokio::task::JoinSet::new();
let _ = Box::new(MyHeadUnit).run(config, &mut js, &setup).await;
}See the docs.rs documentation and the examples/main/ directory for a complete, working reference implementation.
android-auto/
├── src/
│ ├── lib.rs # Library entry point and protocol logic
│ ├── control.rs # Control channel handler
│ ├── video.rs # Video channel handler
│ ├── mediaaudio.rs # Media audio channel handler
│ ├── speechaudio.rs # Speech audio channel handler
│ ├── sysaudio.rs # System audio channel handler
│ ├── avinput.rs # AV input channel handler
│ ├── input.rs # Input channel handler
│ ├── sensor.rs # Sensor channel handler
│ ├── navigation.rs # Navigation channel handler
│ ├── mediastatus.rs # Media status channel handler
│ ├── bluetooth.rs # Bluetooth channel handler
│ ├── common.rs # Shared utilities
│ ├── cert.rs # Built-in TLS certificate
│ └── usb.rs # USB transport (usb feature)
├── examples/
│ └── main/ # Full head unit example application
│ └── main.rs
├── protobuf/ # Protobuf definitions (Bluetooth.proto, Wifi.proto)
└── Cargo.toml
| Type | Role |
|---|---|
AndroidAutoSetup |
Proof-of-initialisation token returned by setup(); must be passed to run() and related methods — ensures initialisation is never skipped |
AndroidAutoConfiguration |
Top-level configuration for the head unit (unit: HeadUnitInfo, optional custom certificate) |
HeadUnitInfo |
Static identity information sent to the phone during handshake |
BluetoothInformation |
Bluetooth adapter MAC address used for wireless negotiation |
NetworkInformation |
Wi-Fi network details relayed to the phone for the wireless session |
SensorInformation |
Set of sensor types the head unit reports to the phone |
VideoConfiguration |
Desired video resolution, FPS, and display DPI |
InputConfiguration |
Supported keycodes and optional touchscreen dimensions |
AudioChannelType |
Discriminates between Media, System, and Speech audio channels |
AndroidAutoMessage |
Enum of all message types that can be received over the link |
SendableAndroidAutoMessage |
Wire-ready message sent from the application back to the phone |
SendableChannelType |
Identifies which channel a SendableAndroidAutoMessage targets |
FrameHeaderType |
Whether a packet fits in a single frame or is fragmented (Single, First, Middle, Last) |
| Trait | Purpose |
|---|---|
AndroidAutoMainTrait |
Core trait — implement to handle connect/disconnect and provide the message sender; requires all channel traits below |
AndroidAutoVideoChannelTrait |
Receive and render H.264 video frames from the phone |
AndroidAutoAudioOutputTrait |
Receive and play audio for media, system, and speech channels |
AndroidAutoAudioInputTrait |
Capture and stream microphone audio to the phone |
AndroidAutoInputChannelTrait |
Handle touch and keycode input binding |
AndroidAutoSensorTrait |
Report sensor data (e.g. night mode, driving status) to the phone |
AndroidAutoNavigationTrait |
Receive turn-by-turn navigation events from the phone |
AndroidAutoWiredTrait |
Marker trait indicating the implementation supports USB connections (usb feature) |
AndroidAutoWirelessTrait |
Bluetooth + Wi-Fi negotiation for wireless connections (wireless feature) |
AndroidAutoBluetoothTrait |
Low-level Bluetooth adapter configuration |
The library relies on:
tokio— async runtimerustls— TLS 1.2/1.3 for the secure channelaws-lc-rs— cryptographic backend used by rustlsprotobuf— message encoding/decodingasync-trait— async trait supportfutures— async combinatorsserde— serialization for message typeslog— structured loggingnusb(optional,usbfeature) — USB device accessbluetooth-rust(optional,wirelessfeature) — Bluetooth RFCOMM for wireless handoff
Contributions are welcome! A few guidelines:
- Fork the repository and create a feature branch.
- Run the tests before submitting:
cargo test - Keep it async — the library is built around Tokio; new I/O code should follow the same pattern.
- Protobuf changes — if you modify
.protofiles underprotobuf/, regenerate the Rust bindings withprotobuf-codegenbefore committing. - Open a pull request with a clear description of what you changed and why.
If you find a bug or want to request a feature, please open an issue.
Licensed under the GNU Lesser General Public License v3.0 or later. See LICENSE for the full text.
This means you can use this library in your own applications — including proprietary or closed-source ones — without being required to release your application's source code. The library itself must remain LGPL.