1 Architecture
David Spitzer edited this page 2026-07-02 16:00:10 -07:00

Architecture

Layering

Changeling mirrors the layering used by aasdk (the open-source library that implements the head unit side of this same protocol), just from the opposite end:

UsbAccessoryConnection      (AOAP bulk pipe as InputStream/OutputStream)
        |
UsbBulkTransport            (frame header + size, fragmentation, encryption hook)
        |
AndroidAutoSession          (routes messages to the right channel handler)
        |
   +---------+------------------+
   |         |                  |
ControlChannel   MediaAudioChannel   (more channels later: input, video, ...)
   |
TlsServerSession   (bridges an SSLEngine through SSL_HANDSHAKE control messages)

Key packages

  • net.spitzertech.changeling.usbUsbAccessoryConnection: permission handling and turning an attached UsbAccessory into streams. This is the only part that talks to Android's USB APIs directly.
  • net.spitzertech.changeling.transport — the wire-format layer: FrameHeader, FrameSize, ChannelId, UsbBulkTransport, and AndroidAutoSession (the orchestrator).
  • net.spitzertech.changeling.transport.cryptoPhoneIdentity (self-signed cert via AndroidKeyStore) and TlsServerSession (drives the TLS handshake through memory buffers instead of a real socket).
  • net.spitzertech.changeling.transport.channels — per-channel protocol handlers (currently just MediaAudioChannel).
  • net.spitzertech.changeling.mediaAudioFileDecoder, standard Android decode APIs, nothing Android-Auto-specific.

The frame format

Every message is wrapped in a small frame:

[2-byte header: channel id + flags][size field][payload]
  • The header's flag byte encodes frame type (FIRST/MIDDLE/LAST/BULK — BULK meaning "fits in one frame"), message type (CONTROL vs SPECIFIC), and encryption (PLAIN vs ENCRYPTED).
  • The size field is 2 bytes (Short) normally, or 6 bytes (Extended: frame size + total message size) on the first frame of a message — including BULK frames, which are treated as "first and only."
  • Messages bigger than aasdk's 0x4000-byte max frame payload are split FIRST → MIDDLE* → LAST and reassembled on the other end.
  • The payload itself is prefixed with a 2-byte big-endian message ID (e.g. VERSION_REQUEST = 0x0001), followed by the message body — raw bytes for a few handshake messages (version negotiation), protobuf for everything else.

TLS handshake direction (the part most likely to surprise you)

The head unit is the TLS client; the phone is the TLS server. This is confirmed from aasdk's source (Cryptor::init() calls setConnectState, i.e. aasdk — playing the head-unit role — is the client) and matches Google's leaked Head Unit Integration Guide. TlsServerSession drives a server-mode SSLEngine, bridging handshake bytes through SSL_HANDSHAKE control-channel messages instead of a real Socket.

Message order after the AOAP/USB session is open:

  1. Head unit sends VERSION_REQUEST → phone replies VERSION_RESPONSE.
  2. Head unit drives a TLS handshake as the client; phone responds as the server (SSL_HANDSHAKE messages, unencrypted framing even though the payload is TLS handshake data).
  3. Once the handshake completes, phone sends AUTH_COMPLETE (now ENCRYPTED framing) followed by SERVICE_DISCOVERY_REQUEST (the phone announcing itself — device_name/device_brand).
  4. Head unit replies SERVICE_DISCOVERY_RESPONSE with its channel list and car/head-unit metadata.
  5. Phone sends CHANNEL_OPEN_REQUEST for whichever channels it wants (e.g. MEDIA_AUDIO); head unit replies CHANNEL_OPEN_RESPONSE.
  6. From there, per-channel protocols take over (e.g. MediaAudioChannel's own SETUP_REQUEST/START_INDICATION/STOP_INDICATION).

Licensing note

The protobuf schema (app/src/main/proto/) is vendored from OpenDsh/aasdk's aasdk_proto directory, which is GPLv3-licensed. This is a deliberate choice — using it made the project GPLv3 as a whole rather than writing an independent, permissively-licensed schema from scratch, in exchange for moving faster. See app/src/main/proto/NOTICE.md in the repo.

What's deliberately not implemented

  • The raw AOAP USB control-transfer handshake (GET_PROTOCOL/SEND_STRING/START) — this is handled entirely by Android's kernel f_accessory gadget driver before any app code runs. Confirmed via research into AOSP source and the kernel driver itself.