DEV Community

Duvan Jamid Vargas
Duvan Jamid Vargas

Posted on

Two Phones, One Dongle, One Car: Building a Phone Switcher for Android Auto (and Knowing When to Stop Reverse Engineering)

It started with a small question: what can we improve on our wireless Android Auto dongle?

Two people, one car, one Carlinkit CPC200-2Air dongle (5.0 / U2AC, AutoKit firmware), and every time we swapped phones it was a manual, slightly miserable dance: turn Bluetooth off on one phone, wait, hope the other one connects first. A lot of poking later we had a full Android app with its own screen inside Android Auto, a BLE presence protocol between phones, a live dongle monitor, a CI pipeline that ships signed releases, and a pile of research notes about what the dongle does inside, including the exact point where we decided to stop.

This is the whole story, dead ends included. The code is MIT-licensed and public: muvatec/switch-phone-aa.

Disclaimer. This is an independent project, not affiliated with Carlinkit, HeWei, Google or Android Auto. It uses the web API the dongle itself exposes for its own settings page, and it contains no manufacturer binaries or code. Use it at your own risk: some actions restart the dongle and may require re-pairing.

1. The problem

A wireless Android Auto dongle stores several paired phones, but it only keeps one linked at a time. When two people share the car, "who gets the car now?" has no first-class answer:

  • The dongle has a list of saved phones, but no "connect to phone X" command.
  • It doesn't tell you which phone is currently connected (more on that below).
  • The phone that was connected before tends to reconnect on its own, because its Android Auto is in automatic mode.

So the goals grew as we learned:

  1. Understand how the firmware works and what could be improved.
  2. See how the dongle behaves without sitting in the car.
  3. Support several phones and switch between them easily, ideally from the car screen.

2. First dead end: speaking the "Carlinkit protocol"

Plugged into a Mac, the dongle enumerates as 12D1:0001 named AutoKit (later VISION after a firmware update), with a vendor-specific interface, bulk endpoints and one interrupt endpoint.

The community has built several open-source clients for these dongles (pi-carplay, FastCarPlay, node-CarPlay) that speak a proprietary protocol with a 0x55AA55AA header, a length, a type and an inverted type. We patched pi-carplay to accept this VID:PID and sent it the usual messages. The dongle answered every message with the same 12 bytes:

0c000000 03000520 <n>
Enter fullscreen mode Exit fullscreen mode

It never sent video, never reported a phase, and reset USB after two to four seconds of silence. We tried the optional USB encryption some newer firmwares use (header 0x55BB55BB, AES-128-CFB). No change.

For a while we assumed this firmware spoke some newer, undocumented variant. We were wrong in an instructive way: those 12 bytes were MTP.

Later, reading the dongle's own plain-text boot scripts, we found start_mtp.sh: in its idle mode (no car attached) the dongle presents itself as an MTP device (mtp-server). Decoding the reply as an MTP container:

Field Value Meaning
length 0x0000000c 12 bytes
container type 0x0003 Response
response code 0x2005 Operation Not Supported
last field <n> the transaction id, copied from whatever bytes we sent

The dongle wasn't speaking a secret protocol. It was politely telling us, twelve bytes at a time, that it didn't support what we were asking, because we were talking to the wrong USB personality. A good reminder to identify the mode of a device before reverse engineering its protocol.

3. The first big hit: the dongle is an Android accessory

Sending the standard USB control request 51 (GET_PROTOCOL) from Android Open Accessory (AOA) returned 2. The dongle presents itself to the car as an Android phone in accessory mode:

  1. It enumerates as 12D1:0001 in its idle MTP mode.
  2. When a "head unit" performs the AOA handshake, scripts kill adbd and mtp-server and the device switches to 18D1:2D00 (accessory).
  3. It then speaks Android Auto 1.7 over TLS 1.2 (ECDHE-RSA-AES128-GCM-SHA256).

That means Google's official Desktop Head Unit (DHU), the tool for testing Android Auto apps without a car, can act as the car, talking directly to the dongle over USB:

DONGLE_SERIAL=<usb serial> tools/run-dhu.sh
Enter fullscreen mode Exit fullscreen mode

Two gotchas we paid for:

  • Always pass the dongle's serial. Without --usb=<serial> the DHU grabs every compatible USB device, including your phone on a cable, and flips it into accessory mode (which also kills USB tethering).
  • For the dongle to stream anything, a phone must already be linked to it over Bluetooth and WiFi, exactly like in a real car.

From that moment we could develop and test the whole thing on a desk.

4. The second big hit: the dongle's web API

The dongle creates a WiFi network (AutoKit_xx) and serves a configuration page on its own address using a small embedded web server (Boa) with a Vue front end. Chrome on Android often forces HTTPS and refuses to load it, which is one reason a native app is useful.

We downloaded the page's JavaScript and read how it talks to the device. Everything goes through one CGI endpoint:

POST /cgi-bin/server.cgi
Content-Type: application/x-www-form-urlencoded

cmd=<command>&[item=<key>&val=<value>&]ts=<epoch ms>&sign=<md5>
Enter fullscreen mode Exit fullscreen mode

The signature is an MD5 over the sorted fields plus a constant that the page's own JavaScript sends to every client, so it is an integrity check on the form, not a secret. (A first 400 Bad Request turned out to be a missing /cgi-bin/ prefix.) The commands we use:

cmd What it does
infos Everything: BoxInfo, Settings, DevList (saved phones), WifiChannelList, ModuleInfo
set (item, val) Change a setting
restart Reboot the dongle (~20 s)
BoxMonitor CPU rate/temperature/frequency, memory rate, WiFi RX/TX in Mbps
carInfo, syncTime, logFile ... Car metadata, time sync, logs
reset Factory reset (we don't touch it lightly)

From infos we could finally see the real firmware version, read every setting, and change a couple (fastConnect, mediaDelay). We also discovered what the API doesn't give us, which shaped the entire design.

5. What the dongle knows, and what it doesn't

  • DevList stores paired phones, but there is no "connect to X" command.
  • There is no reliable "who is active" field. MDLinkType says AndroidAuto when linked, but MDModel comes back empty, and the index field in DevList lies (we first used it and marked the wrong phone as active).
  • A saved phone may have no name; you learn it when the phone connects, or rename it by hand.

So how do you switch? Restart the dongle. When it comes back, whichever saved phone is actively looking connects first. With both people nearby this worked with no tricks, in about 30 seconds: ~5 s to shut down, ~21 s to boot, ~5 s for the new phone to connect.

The remaining problem: who is where? The dongle can't say. So the phones have to.

6. Presence over Bluetooth Low Energy

Every phone running the app advertises its own state over BLE and listens for the others. There is no GATT connection; everything rides in a non-connectable advertisement:

service data (9 bytes) = hash(4) + flags(1) + target(4)

hash   = first 4 bytes of MD5(lowercase Bluetooth name)
flags  = bit0 projecting Android Auto
         bit1 requesting control
         bit2 rejected
         bit3 accepted
target = hash of the phone this message is addressed to (00000000 = nobody)
Enter fullscreen mode Exit fullscreen mode

The 4-byte hash is matched against the names the dongle has saved, which yields three states per phone:

State How it's decided
ACTIVE The phone itself knows (CarConnection says it's projecting); others see the projecting flag in its beacon
AVAILABLE Saved in the dongle and its beacon was heard in the last 45 s
SAVED Saved in the dongle, not heard

The request / accept protocol

  1. Phone B sees that A is projecting and offers Switch to this phone (or the user taps Request connection in the app, in a quick-settings tile, or from a launcher shortcut).
  2. B sets the requesting bit for 45 s, addressed to A.
  3. A hears it and shows an accept / reject prompt: a heads-up notification delivered through Android Auto itself (so it reaches the driver even while Waze or Spotify is on screen), a modal in the app, and a regular notification.
  4. If A accepts, it asks the dongle to restart. B, now the only phone looking, connects.
  5. A answers over the same channel for 30 s with accepted or rejected, so B can show what happened and let the user retry.
stateDiagram-v2
    [*] --> IDLE
    IDLE --> WAITING: user requests control
    WAITING --> ACCEPTED: A accepts
    WAITING --> REJECTED: A rejects
    WAITING --> TIMEOUT: no answer in 45 s
    REJECTED --> WAITING: retry
    TIMEOUT --> WAITING: retry
    ACCEPTED --> IDLE: phone B is now projecting

The security trade-off we chose

The beacon isn't authenticated: anyone who imitates a saved phone's 4-byte hash could ask for a switch. So the active phone must approve. Auto-accept exists, but is off by default, and the contribution rules forbid changes that enable anything that gives up the car without confirmation. The beacon contains only a hash of the Bluetooth name, no personal data.

7. Drawing our own screen in the car

Our first car UI used the standard Android for Cars templates (IoT category): a list and a grid. It worked, and it looked like every other templated car app. You can't change backgrounds or cards.

We wanted rounded cards, light/dark theme, colored states. The only way to draw freely on the car screen is the navigation category: you receive a Surface through a SurfaceCallback and paint on it with a Canvas. Touches arrive through SurfaceCallback.onClick; the theme comes from CarContext.isDarkMode.

The costs, stated plainly:

  • It needs the androidx.car.app.NAVIGATION_TEMPLATES and ACCESS_SURFACE permissions.
  • Navigation apps have extra review requirements on Google Play, so for now we distribute an APK and rely on Android Auto's Unknown sources developer option.

The honest failure: it didn't show up in a real car

Tested in a real car with a Pixel 9 and Unknown sources enabled, the app did not appear in the car's launcher or in "Customize launcher". In the DHU it appeared perfectly. The system log buffer (256 KB) had already rolled over by the time we looked, so we couldn't see why.

Our hypothesis (not a confirmed fact): Android Auto is stricter with the navigation category for sideloaded apps; other sideloaded navigation apps report the same. What we did about it:

  • Added a second entry in the IoT category with standard templates as a fallback.
  • Added CarLog, a persistent 200 KB log of what Android Auto does with the app (service created, host, screen, size, touches) that can be shared from the app, so the next attempt has evidence.

Everything has been validated with the DHU and the dongle on a desk. We say so in the repo.

8. Bugs that taught us something

A short list of real ones from the project log:

Symptom Cause Fix
App crashed when opening in Android Auto A Row without a title, because the dongle returned a phone with no name Always provide a label fallback
Crash when switching phones A template in loading state can't carry an icon or actions A separate template for the loading state
"Active" marked the wrong phone We trusted DevList.index Each phone reports about itself
The idle phone gave up the car without asking First design executed requests directly RequestInbox plus explicit accept/reject
No way to retry The requester never learned the result BLE response and WAITING / REJECTED / TIMEOUT / ACCEPTED states
"No requests load" on phone #2 Its Bluetooth had been turned off; presence only started once at app launch Presence restarts with Bluetooth state; the screen now says Bluetooth off / Missing permissions / Searching...

Two longer investigations are worth telling.

A new phone wouldn't connect. Android Auto's logs showed WIRELESS_SETUP_FAILED_TO_START_NO_HFP_FROM_HU_PRESENCE: the Bluetooth link to the dongle dropped after ~2 s, and wireless Android Auto requires the dongle's hands-free profile (HFP). The dongle serves one Bluetooth connection at a time. Turning off Bluetooth on the other phones, forgetting the pairing and re-pairing with "Phone calls" enabled fixed it.

Android Auto felt slow on one phone. The link itself was healthy: audio acks around 23 ms, 5 GHz WiFi at 270 to 300 Mbps. But the phone was nearly out of RAM with swap at 92 %, system_server at 94 % CPU, and Android Auto's frame limiter flapping between 60 and 30 fps. The dongle and the WiFi were innocent. That's why the app now measures the phone's own memory and warns, next to a live dongle monitor (temperature, CPU, memory, WiFi throughput with an overheating alert).

9. The wall: server.cgi

The obvious next question: is there a hidden command to switch phones without a 30-second reboot? The CGI binary behind that API might know.

We took the public 2024.10 firmware published by the community (the same community whose work made much of this possible: ludwig-v/wireless-carplay-dongle-reverse-engineering) and ran strings on server.cgi. Nothing useful. The reason:

Fact Value
Format 32-bit ARM EABI5 static ELF, no section headers
Size 42,304 bytes
Entropy ~7.9 bits/byte almost everywhere (compressed or encrypted)
strings 478 lines, all noise

A small disassembly of the loader (725 ARM instructions, with capstone) showed a UPX-style in-memory self-extractor with LZMA, plus a hardware-encryption layer:

  1. Open /proc/self/exe and locate the payload.
  2. mmap2 an anonymous buffer.
  3. If the payload is large enough, open /dev/hwaes (the string is assembled byte by byte so it isn't greppable) and call ioctl(fd, 0xC00C6206, {src, dst, len}): a request served by a kernel driver, not by code in the file. The key never appears in the binary.
  4. Decompress LZMA, cacheflush, mprotect, and jump into the real program.

We also checked the USB kernel-module archive of the same firmware (g_android_accessory.ko, g_android_autobox.ko, g_iphone.ko and friends: Linux 3.14.52, ARMv7, GPL). None contains hwaes. The decryption driver presumably lives inside the kernel image.

Where we deliberately stopped

Going further means recovering device-bound cryptographic material from the manufacturer's hardware. We chose not to. That's a line we draw on purpose, not a limitation we lament, even though it's "our own" dongle and even though a plausible next step existed (unpacking the kernel image). The repository's contribution rules say the same: no code that tries to decrypt, decompile, or extract key material from the firmware binaries, and no manufacturer binaries in the repo. Static analysis of plain-text scripts and unencrypted binaries is welcome. The analysis was purely static: we never ran the binary, and we never tried guessed commands against the dongle's API (an unknown command could reboot it or worse).

10. What the plain-text scripts told us

Everything legible was enough to map the system. The key script is phone_link_deamon.sh <CarPlay|AndroidAuto|HiCar|ICCOA> <start|stop>. For Android Auto it:

  • copies the binaries to RAM (/tmp/bin, /tmp/lib; the flash is read-only),
  • starts hfpd (Bluetooth hands-free) in the background and ARMAndroidAuto in the foreground,
  • and runs inside a supervisor loop: while a lock file exists, every second it relaunches whatever died.

That implies a much lighter "soft switch" than a full reboot: kill the two link processes without removing the lock file and the supervisor restarts the link in 1 to 2 seconds. The candidate sequence, run inside the dongle, would look like:

hcitool dc <phone MAC>          # drop the Bluetooth link of the active phone
killall hfpd ARMAndroidAuto     # the supervisor relaunches them within ~1 s
Enter fullscreen mode Exit fullscreen mode

This is not implemented and has never been run. There is no shell on the dongle in car mode (adbd is killed when the AOA handshake happens), and we don't know whether the car session survives, nor which phone ARMAndroidAuto picks after relaunch. It's documented as future work that would require a modified firmware, with the open questions written down.

11. A detour that didn't pan out: internet for passengers

We also tried to share the phone's mobile data with everyone in the car. Android lets a normal app neither switch the hotspot on nor set its name and password (that needs privileged permissions that only the shell user from adb has). We built a card with name, password, QR code and a notification button, realized the configuration was still manual, so it added nothing, and removed it. The open paths (Shizuku, a 4G router, or a firmware that makes the dongle a bridge) are written down so nobody repeats the experiment.

12. Shipping it

  • Stack: Kotlin, Jetpack Compose (Material 3) for the phone UI, AndroidX Car App Library 1.4, minSdk 26; roughly 3,000 lines of Kotlin in 16 files.
  • HTTP in clear text only to the dongle's own address (it doesn't support HTTPS); the network security config allows it for that single IP.
  • Never auto-update the dongle's firmware from the app. The downside (a bricked dongle) is much bigger than the upside.
  • CI: every push to main builds the app and publishes a GitHub release (v0.1.<run number>) with the APK, and an Android App Bundle for Google Play. Releases are signed with a release key kept in a self-hosted secrets manager; CI authenticates to it with GitHub OIDC, with no stored secret (I wrote about that setup in Stop Storing Secrets in GitHub Actions). One lesson: the first releases were signed with the debug key, whose signature changes between runs, so Android refuses to update in place. You had to uninstall first.

13. What the app does today

  • Multi-phone: any number of phones can share the dongle, with ACTIVE / AVAILABLE / SAVED states.
  • One-tap switching from the app, from the Android Auto screen, from a quick-settings tile or from an icon shortcut.
  • Request protocol with accept/reject and retry.
  • Visible countdown during the ~30 s reboot, with phases and a progress bar in the app, the car screen and notifications.
  • Dongle settings built dynamically from the keys your firmware reports, using the exact labels and help texts of the manufacturer's own interface (names, connection, audio and calls, video, WiFi channel, language). Firmwares differ, so the app only shows what the firmware exposes.
  • Live monitor with donut charts and an overheating warning.
  • Phone memory monitor.
  • Tools: custom logo, car data, log download, restore.
  • A guide for adding a new phone, with automatic detection when it registers.
  • Car emulation without a car through the DHU in direct AOA mode.

Known limits (all in docs/LIMITATIONS.md): switching reboots the dongle, "available" only works for phones that run the app with Bluetooth and permissions, BLE range is roughly 10 to 30 m (plenty inside a car), each firmware update can hide or rename settings, and distribution is by APK for now.

14. Try it, break it, contribute

If you own a Carlinkit/AutoKit dongle:

  1. Try the app. Download the latest APK from the releases page and install it on every phone that shares the dongle.
  2. Tell us your model and firmware if it differs from ours (CPC200-2Air, firmware 2025.12.29). Setting keys vary between firmwares, and the app is designed to adapt to that.
  3. Contribute. Bug fixes with reproduction steps, support for other dongle models, UI improvements, new dongle settings (always with the label and help text exactly as the dongle's own interface shows them, and a citation), translations and screenshots. Read CONTRIBUTING.md first, and the project log docs/CONTEXT.md so we don't walk the same dead ends twice.

The docs are currently written in Spanish (the research notes, the architecture and the full project log). Translations are a very welcome kind of PR.

Everything is in the repo:

  • Project log, dead ends included: docs/CONTEXT.md
  • Architecture: docs/ARCHITECTURE.md
  • Dongle web API: docs/DONGLE_API.md
  • How the dongle works inside: docs/RESEARCH.md, docs/SOFT_SWITCH.md, docs/SERVER_CGI.md
  • Limitations: docs/LIMITATIONS.md

Repository: github.com/muvatec/switch-phone-aa, MIT license. Issues and PRs are welcome.

Top comments (0)