Android Auto 手机端应用的开源实现。此应用在手机上运行,通过USB将画面投射到汽车头机,替代Google专有的 com.google.android.projection.gearhead APK。
本项目处于早期开发阶段。协议握手和视频投影已在真实头机上成功运行。手机屏幕可在汽车头机上成功显示约几秒钟后断开(视频稳定性正在改进)。
请自行承担风险使用。 本软件按“原样”提供,不提供任何形式的担保。
USB Plug-in → MainActivity → ProjectionService ↓ UsbAoaTransport (USB AOA accessory mode) ↓ MessageFramer (16KB frame fragmentation) ↓ InBandTls (TLSv1.2 via SSLEngine) ↓ ProtocolEngine (AAP state machine) ↓ ┌───────────┼───────────┐ Video Input Audio (H.264) (touch/keys) (PCM)
## 已知问题
- **通道分配假定顺序** — 我们分配 `SERVICE_DISCOVERY_RESPONSE` 中的第一个 `av_channel` 为视频,第二个为音频。这在车载主机上有效(通道1 = 视频),但在 openauto 上失败(通道4 = 音频,而非视频)。修复方案:解析 `av_channel` 内的 `stream_type` 字段以区分 `VIDEO(3)` 和 `AUDIO(1)`。
- **视频稳定性** — 由于车载主机 USB 缓冲区溢出,长时间流传输后连接会断开。参见下面的测试结果。
- **车载主机上设备重复显示** — 车载主机的智能手机页面将我们的应用显示为两个独立条目(一个用于 Android Auto,一个用于蓝牙),而非同时显示这两种功能的单个条目。这是由于 Android 12+ 阻止访问真实的蓝牙 MAC 地址(返回 `02:00:00:00:00:00`)。临时解决办法:通过 `adb shell "echo $(adb shell settings get secure bluetooth_address) > /sdcard/Android/data/org.openandroidauto/files/bt_address.txt"` 将真实地址写入配置文件。需要一个 UI 设置界面让用户手动输入其蓝牙 MAC 地址。
- **语音助手按钮未被处理** — 当驾驶员按下车载主机上的语音/助手按钮时,我们收到 `VOICE_SESSION_REQUEST` 并尝试启动一个语音助手(Dicio 或系统默认)。但启动的助手尚未接收到来自车载主机麦克风的音频。
### 视频稳定性测试结果
测试图案(彩条)在 800x480 分辨率下,I帧间隔 1 秒:
| FPS | 比特率 | 分片 | 持续时间 | 帧数 | 状态 |
|-----|--------|------|----------|------|------|
| 30 | 2Mbps | 否 | ~3s | ~90 | ❌ 过快 |
| 15 | 2Mbps | 否 | ~33s | ~500 | ⚠️ 较好 |
| 10 | 2Mbps | 否 | ~93s | ~930 | ⚠️ 良好 |
| 30 | 2Mbps | 是(2KB) | 5-25s | 150-750 | ⚠️ 不稳定 |
| 30 | 500Kbps | 是(2KB) | ~54s | ~1691 | ⚠️ 较好 |
| 15 | 250Kbps | 是(2KB) | ~67s+ | 1000+ | ⚠️ 良好 |
| 30 | 250Kbps | 是(2KB), I=5s | ~20s | ~600 | ❌ 较长 I帧时更差 |
| 15 | 250Kbps | 否 | ~13s | ~200 | ❌ 分片在此有帮助 |
根本原因:车载主机 USB 接收缓冲区在持续高吞吐量下溢出。较低的数据速率意味着更长的连接时间。
**最佳确认配置:** 10fps,2Mbps,无分片 = 93 秒。分片后加密的实现存在缺陷(车载主机无法重组)——需要进一步调查。
## 构建```bash
./gradlew assembleDebug
需要 Android SDK 及平台 35。
./gradlew testDebugUnitTest
113 个单元测试和集成测试,涵盖协议、帧、TLS、通道逻辑、视频状态机、传感器处理和触摸输入。
### 使用 openauto(Docker)进行集成测试
openauto 是一个第三方车机模拟器,支持完整的 Android Auto 协议。我们用它来验证我们的协议实现,无需真实车辆。
#### 前提条件
- Docker 已安装并运行
- 手机通过 ADB 连接(USB 或无线)
- 已在手机上安装应用:`./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk`
#### 1. 构建 openauto Docker 镜像(一次性)```bash
cd thirdparty/openauto
docker build -f Dockerfile.headless -t openauto-headless .
此命令将在 Debian 容器中构建 openauto 及其所有依赖项(Qt5、boost、protobuf、OpenSSL)。首次构建大约需要5分钟。
docker run --rm -p 5100:5000 -e QT_QPA_PLATFORM=offscreen
openauto-headless timeout 60 /src/build/bin/autoapp
openauto 在容器内监听端口 5000,映射到主机的端口 5100。它以无头模式运行(无需显示器)。
#### 3. 设置 ADB 反向端口转发```bash
adb reverse tcp:5000 tcp:5100
这使得手机的 localhost:5000 隧道连接到计算机的 localhost:5100 (openauto)。当没有找到 USB 附件时,我们的应用作为 TCP 客户端连接到 localhost:5000。
adb shell am start -n org.openandroidauto/.MainActivity
该应用将会:
1. 无法找到USB配件
2. 连接到 `localhost:5000`(通过adb reverse的openauto)
3. 执行完整的协议握手(VERSION → TLS → AUTH → SERVICE_DISCOVERY)
4. 打开通道(视频、音频、输入、传感器)
5. 开始流式传输视频(测试图案)
#### 5. 在openauto日志中验证
你应该在openauto输出中看到:```
[OpenAuto] handleNewClient() - Handle WIFI Client Connection
[OpenAuto] [AndroidAutoEntity] Send Version Request.
[OpenAuto] [AndroidAutoEntity] onVersionResponse()
[OpenAuto] [AndroidAutoEntity] Beginning SSL handshake.
[OpenAuto] [AndroidAutoEntity] Handshake completed.
[OpenAuto] [AndroidAutoEntity] onServiceDiscoveryRequest()
[OpenAuto] [AndroidAutoEntity] onAudioFocusRequest()
[OpenAuto] [AudioMediaSinkService] onChannelOpenRequest()
[OpenAuto] [VideoMediaSinkService] onChannelOpenRequest() (if video focus granted)
adb pull /sdcard/Android/data/org.openandroidauto/files/aa_log.txt cat aa_log.txt
日志文件在 USB 切换之间持久保存在手机上(在真实车载主机上测试时很有用)。
#### 快速单行测试```bash
# Assumes openauto image already built and app installed
docker run --rm -p 5100:5000 -e QT_QPA_PLATFORM=offscreen openauto-headless timeout 20 /src/build/bin/autoapp &
sleep 3 && adb reverse tcp:5000 tcp:5100 && adb shell am start -n org.openandroidauto/.MainActivity
Message Id not Handled: 4 — 这是已知的 openauto 特性,非错误Android Auto 协议通过多个项目逆向工程得来。每个项目都基于前者的工作:
opencardev/aasdk 相比 AACS 新增的内容:
关键结构差异:
priority + channel_id;aasdk 使用 priority (sint32) + service_id本项目的权威协议参考位于 thirdparty/aasdk/protobuf/ 目录。
Android Auto 使用双向 TLS。手机充当 TLS 服务器,必须提供由 Google Automotive Link CA(嵌入在车机固件中)签发的证书。没有正确的私钥,车机会拒绝连接并返回 AUTH_COMPLETE status=-3。
手机呈现一个由两个证书组成的链:
O=CarService,由 Google Automotive Link CA 签名O=Google Automotive Link(有效期 2014-2044)Google 似乎每隔约 8 个月轮换一次嵌入在 Android Auto APK 中的证书+私钥(与证书有效期一致)。这可能是限制提取密钥使用期限的刻意措施——如果车机检查证书过期时间,旧的提取密钥将失效。官方应用的用户通过应用更新获得新证书。如果该理论成立,不更新官方应用的用户最终可能会被检查过期时间的车机拒绝。并非所有车机都检查过期时间——具体行为取决于型号。
私钥在 Android Auto APK 内以 AES-256-CBC 加密存储。车机会验证手机的证书是否由 Google Automotive Link CA 签名——任何由该 CA 签名的证书都会被接受。
该密钥嵌入(加密)在 Android Auto APK 中,并可使用 APK 自身的算法解密。这需要一台具有 ADB 访问权限的 Android 设备(无需 root,无需 Google Play 服务)来运行解密,因为 Android 的 Base64 解码器行为与桌面 JVM 不同。
需求:
adb pull 从手机拉取,或从 APKPure/APKMirror 下载)d8 构建工具,adb)步骤:
adb pull $(adb shell pm path com.google.android.projection.gearhead | grep base | cut -d: -f2) aa.apkdalvikvm 在设备上运行查找证书提供者类(第 2 步):
类名是混淆的,并且在不同 APK 版本之间会变化,但结构始终相同。在 JADX 中搜索 "-----BEGIN CERTIFICATE-----"——您会找到一个实现接口的小类,该接口包含三个方法:
a() → 返回一个 String(CarService 证书 PEM)b() → 返回一个 byte[](约 1712 字节——AES 加密的私钥)c() → 返回一个 byte[](256 字节——KDF salt)按版本的已知类名:
解密函数位于附近的类中——搜索 "AES/CBC/PKCS5Padding" 即可找到。它接受证书提供者接口作为参数。
注意: 解密步骤(第 4 步)只需要 dalvikvm——任何具有 ADB 的 Android 设备都可以,无需 root 或 Google Play 服务。GApps 的要求仅适用于第 1 步(拉取 APK,因为 AA 应用通过 Play 商店分发)。
注意: 您不需要修改或运行反编译的 APK 代码。相反,您编写一个独立的 Decrypt.java 类,重新实现解密逻辑,从文件中读取提取出的字节数组,并拥有自己的 main() 入口点。反编译的源码仅用作参考,以了解算法并复制字节数组。完整的 Decrypt.java 源码请参阅 tools/decrypt_key_from_apk.md。
关键的 JADX 错误: JADX 将 KDF 辅助函数反编译为 byte b = bArr2[i2] & 255;,但必须为 int b = bArr2[i2] & 255;。byte 类型会截断回有符号数,产生垃圾输出。修复为 int 后解密即可正常工作。
KDF(tweakBytes/ap)函数:```java
static void tweakBytes(byte[] bArr, byte[] bArr2, byte[] bArr3) {
for (int i = 0; i < bArr.length; i++) {
for (int i2 = 0; i2 < 48; i2++) {
int b = bArr2[i2] & 255; // MUST be int, not byte
bArr2[i2] = (byte) (((((b >> 7) | (b + b)) + 33) ^ bArr3[i2 % bArr3.length]) ^ bArr[i]);
}
}
}
在AES解密之后,`T()` 函数提取密钥:
- 跳过前28字节,删除最后26字节
- 对中间部分进行Base64解码(URL_SAFE,标志位=2)
- 结果是一个PKCS#8 DER编码的RSA私钥
**注意:** 由于 `android.util.Base64` 与 `java.util.Base64` 的差异,必须在Android上运行(而不是桌面JVM)。桌面JVM的 `Base64.getUrlDecoder()` 会拒绝Android解码器所接受的标准base64字符(`+`、`/`)和换行符。在桌面上使用 `Base64.getMimeDecoder()`,或者使用 `dalvikvm` 在设备上运行解密:```bash
# Compile to DEX and run on any Android device with ADB access
javac Decrypt.java -d out
d8 out/Decrypt.class --output dex_out
adb push dex_out/classes.dex /data/local/tmp/decrypt.dex
adb shell "dalvikvm -cp /data/local/tmp/decrypt.dex Decrypt"
输出 PKCS#8 私钥的 base64 形式。将其包装在 PEM 头中,并放置在 app/src/main/assets/carservice_key.pem。
完整的分步指南请参阅 tools/decrypt_key_from_apk.md。
由于某些车载主机可能不会检查证书过期时间,先前提取的 cert+key 对(即使已过期)可能仍然有效。来源:
[email protected](参见 gamelaster/opengal_proxy)KeyFactory.generatePrivate()(需要在同一设备上同时拥有 root 权限和 Google Play 服务): ```bash
frida -U -n "com.google.android.projection.gearhead" -l tools/dump_key_frida.js
一旦获得,将证书和密钥放在 app/src/main/assets/carservice_key.pem。
参见 tools/dump_key.sh 和 tools/dump_key_frida.js 了解运行时提取脚本。
本项目基于 GNU General Public License v3.0 授权。
本项目包含来自 aasdk 的协议缓冲区定义(GPLv3, 版权所有 © 2018 f1x.studio / Michal Szwaj)作为 git 子模块。
| 项目 | 年份 | Proto 文件 | 角色 |
|---|
| f1xpl/aasdk | 2018 | 1 个整体文件 (Wifi.proto) | 原始逆向工程 — 核心协议、视频、音频、输入、传感器 |
| AACS | 2020 | 28 个(按消息拆分) | 手机端实现 — 最简覆盖视频投射 |
| opencardev/aasdk | 2024 | 254 个(按服务分层) | 权威参考 — 包含所有服务的完整协议 |
| APK 版本 | 证书提供者类 | Salt+key 类 | 解密类 |
|---|
| v6.4 | SslWrapper(字段 o、p) | 同一类 | SslWrapper.m23915f() |
| v16.8 | ivo / rqi | ivq / rql(字段 b、c) | ivq.d() |