深入浅出 Android AOA 协议:通信流程与设备切换附着机制解析(含代码实战)
在进行车载开发(如 CarPlay、Android Auto、HiCar等盒子互联)或外设硬件通信时,AOA(Android Open Accessory)协议是我们经常需要打交道的核心技术。
AOA 协议允许外部 USB 硬件设备主动与 Android 设备建立连接。很多开发者在初次接触 AOA 时,往往会被其繁琐的"权限申请、模式切换、断开重连"流程绕晕。本文将详细梳理 AOA 协议的完整通信流程,并结合具体的底层逻辑代码,深度拆解 AOA 设备是如何完成"身份切换与重新附着"的。
一、 核心难点:AOA 的"变身"机制
AOA 连接并不是一蹴而就的单向流程,它需要经历一个"普通 USB 识别 -> 握手切换模式 -> 底层断开重启 -> 重新以 AOA 设备枚举"的生命周期。
在这个过程中,最核心的机制就是设备身份的切换。为了保证代码的健壮性与逻辑清晰,通常需要多个不同维度的接收器(Receiver)或状态机来协同处理这几次插拔。以下将结合具体代码,还原这套机制的真实流转过程。
二、 流程拆解与代码实战
1. 第一阶段:物理附着与 AOA 握手切换
当 USB 盒子刚插上 Android 车机或手机时,系统识别到的仅仅是一个普通的物理 USB 设备。此时,作为前哨站的监听器(如 UsbStateBroadcastReceiver)会捕获到 ACTION_USB_DEVICE_ATTACHED 广播。
在确认拥有 USB 权限后,应用层必须主动向该设备的端点发送标准的 USB 控制指令(Control Transfer),告知外设进入 AOA 模式。
关键代码实现:
向外设写入厂商、型号等信息(52指令),并发送启动 AOA 模式的指令(53指令)。
Kotlin
kotlin
/**
* 判断是否是AOA模式的设备,如果是直接初始化通讯,否则激活AOA模式
*/
private fun switchOrInitDevice(device: UsbDevice) {
val isAoaDevice = isAoaDevice(device)
if (isAoaDevice) {
initDevice(device)
} else {
// 根据AOA协议打开Accessory模式
mUsbManager?.openDevice(device)?.let { connection ->
listOf(
"MANUFACTURER" to 0, // MANUFACTURER
"MODEL" to 1, // MODEL
"DESCRIPTION" to 2, // DESCRIPTION
"1.0" to 3 // VERSION
).forEach { (string, index) ->
// 发送 52 指令配置设备信息
connection.controlTransfer(0x40, 52, 0, index, string.toByteArray(), string.length, 100)
}
// 发送 53 指令触发底层模式切换
val buffer = byteArrayOf(0x00.toByte(), 0x01.toByte())
val usbInterface = device.getInterface(0)
connection.controlTransfer(
0x40, // USB_DIR_OUT | USB_TYPE_VENDOR
53, // ACCESSORY_SET_AUDIO_MODE
0, // 0表示不启用音频
0,
buffer,
buffer.size,
1000
)
// 释放并关闭当前物理连接,等待设备重启
connection.releaseInterface(usbInterface)
connection.close()
}
}
}
此时,盒子收到 53 指令后,会主动在底层断开 USB 连接。系统会发出 DETACHED 广播,这属于正常现象,代表盒子正在"变身"。
2. 第二阶段:身份重置与重新附着(Attach)
当盒子完成底层重启,再次挂载到 Android 系统时,它的身份已经改变。此时,我们需要拦截这个新的 AOA 身份,并打通底层数据管道。
关键代码实现:
通过校验 vendorId(Google 官方通常为 0x18D1)和 productId(如 0x2D00 或 0x2D01)来拦截重连设备。确认身份后,遍历底层的 UsbEndpoint,找出负责批量传输(Bulk Transfer)的输入和输出端点。
Java
ini
// AoaUsbBroadcastReceiver.java 中的核心拦截与初始化逻辑
private final int AOA_ACCESSORY_VID = 0x18D1;
private final int AOA_ACCESSORY_PID = 0x2D00;
private boolean isAoaDevice(UsbDevice device) {
if (device != null) {
return device.getVendorId() == AOA_ACCESSORY_VID &&
(device.getProductId() == AOA_ACCESSORY_PID || device.getProductId() == 0x2D01);
}
return false;
}
private void initDevice(UsbDevice device) {
if (mUsbManager != null) {
mUsbDeviceConnection = mUsbManager.openDevice(device);
if (mUsbDeviceConnection != null) {
mUsbInterface = device.getInterface(0);
if (mUsbInterface != null) {
int endpointCount = mUsbInterface.getEndpointCount();
for (int i = 0; i < endpointCount; i++) {
UsbEndpoint endpoint = mUsbInterface.getEndpoint(i);
// 寻找 Bulk 批量传输端点
if (endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) {
if (endpoint.getDirection() == UsbConstants.USB_DIR_OUT) {
mUsbEndpointOut = endpoint; // 赋值输出端点
} else if (endpoint.getDirection() == UsbConstants.USB_DIR_IN) {
mUsbEndpointIn = endpoint; // 赋值输入端点
}
}
}
if (mUsbEndpointIn != null && mUsbEndpointOut != null) {
isReceiverMessage = true;
receiverMessage(); // 启动数据接收线程
}
}
}
}
}
3. 第三阶段:上层业务通道建立 (Accessory API)
除了直接操作 UsbEndpoint 进行批量传输外,Android 还提供了封装度更高的 UsbAccessory API。在某些架构中(如通过 AccessoryBroadcastReceiver 处理),当设备成功切换为 AOA 模式后,系统也会将其识别为 UsbAccessory。
此时,可以通过获取文件描述符(ParcelFileDescriptor)的方式,将其转换为我们熟悉的 FileInputStream 和 FileOutputStream 进行数据读写。
关键代码实现:
Java
ini
private void openAccessory(UsbAccessory accessory) {
ParcelFileDescriptor parcelFileDescriptor = mUsbManager.openAccessory(accessory);
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
if (fileDescriptor != null) {
// 建立输出流(用于向盒子发送业务指令,如 Carplay/HiCar 初始化)
fileOutputStream = new FileOutputStream(fileDescriptor);
// 开启子线程建立输入流,循环读取底层回传的数据包
thread = new Thread() {
@Override
public void run() {
try {
bufferedInputStream = new BufferedInputStream(new FileInputStream(fileDescriptor), 1024);
started = true;
while (started) {
byte[] buffer = new byte[1024];
int read = bufferedInputStream.read(buffer);
// 接收数据,后续交由 MessageCodecUsb 等业务类进行解包(Header + Content)
}
} catch (Exception e) {
clearAccessory();
}
}
};
thread.start();
}
}
总结
AOA 通信中最容易让人迷惑的就是"插上 -> 发指令 -> 断开 -> 变身再插上"的假象流转。
在代码架构设计上,理清 UsbDeviceConnection.controlTransfer 触发模式切换(阶段一)、通过 VID/PID 校验拦截二次附着并分配 UsbEndpoint(阶段二),以及利用 FileInputStream/FileOutputStream 桥接上层业务流(阶段三)的关系,是稳定处理 CarPlay / HiCar 等车机互联协议的基石。