Android 车载 AOA 开发笔记:两个 Android 设备通过 USB Host 与 Accessory Mode 双向通信

3 阅读4分钟

Android 车载 AOA 开发笔记:两个 Android 设备通过 USB Host 与 Accessory Mode 双向通信

Android Open Accessory(AOA)可以让两个 Android 设备通过 USB 建立双向通信,其中一个 Android 作为 USB Host,另一个 Android 作为 USB Device,并由 Host 主动将 Device 切换到 Accessory Mode。典型链路:Android Host → UsbManager → AOA 51/52/53 → Android Device → Accessory Mode → Bulk IN/OUT → 双向数据通信。Host 端主要使用 UsbManager + UsbDevice + UsbDeviceConnection + UsbInterface + UsbEndpoint,Device 端主要使用 UsbManager + UsbAccessory + ParcelFileDescriptor

AOA 通信流程

完整流程:Host 枚举 Android Device → 获取 UsbDevice Permission → openDevice → GET_PROTOCOL 51 → SEND_STRING 52 → ACCESSORY_START 53 → Device USB 断开并重新枚举 → Host 发现 18D1:2D00/2D01 → 获取新的 UsbDevice Permission → claimInterface → Bulk IN/OUT → Device 获取 UsbAccessory → openAccessory → InputStream/OutputStream

可以简单记录为:51:查询是否支持 AOA52:发送 Host 身份信息53:要求 Device 切换到 Accessory Mode

Host 与 Device

Host Android 使用:UsbDevice + UsbDeviceConnection + controlTransfer() + UsbEndpoint + bulkTransfer()

Device Android 使用:UsbAccessory + openAccessory() + ParcelFileDescriptor + FileInputStream/FileOutputStream

因此:Host 看到 UsbDeviceDevice 看到 UsbAccessory

Host 条件

Host Android 需要声明:

<uses-feature android:name="android.hardware.usb.host" android:required="true" />

硬件接口必须实际工作在 USB Host Role,另一台 Android 必须工作在 USB Device Role。两个 Android 使用 USB-C 连接不代表角色一定正确,实际还受到 USB Controller + Type-C Role + OTG + Cable + System USB Configuration 影响。

GET_PROTOCOL

Host 首先发送 Vendor Control Request:

bmRequestType = USB_DIR_IN | USB_TYPE_VENDORbRequest = 51wValue = 0wIndex = 0wLength = 2

核心代码:

byte[] protocol = new byte[2];
int length = connection.controlTransfer(UsbConstants.USB_DIR_IN | UsbConstants.USB_TYPE_VENDOR, 51, 0, 0, protocol, 2, 1000);
int version = length == 2 ? (protocol[0] & 0xFF) | ((protocol[1] & 0xFF) << 8) : -1;

version > 0 才继续 AOA 握手。如果 Request 51 失败,不应该继续发送 52 和 53。

SEND_STRING

Host 使用 Request 52 向 Device 发送 Accessory Identification。

String ID:0:Manufacturer1:Model2:Description3:Version4:URI5:Serial

发送一个字符串的核心逻辑:

byte[] data = Arrays.copyOf("AndroidHost".getBytes(StandardCharsets.UTF_8), "AndroidHost".getBytes(StandardCharsets.UTF_8).length + 1);
connection.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, 52, 0, 0, data, data.length, 1000);

实际依次发送:

byte[] manufacturer = Arrays.copyOf("AndroidHost".getBytes(StandardCharsets.UTF_8), "AndroidHost".getBytes(StandardCharsets.UTF_8).length + 1);
connection.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, 52, 0, 0, manufacturer, manufacturer.length, 1000);
byte[] model = Arrays.copyOf("VehicleHost".getBytes(StandardCharsets.UTF_8), "VehicleHost".getBytes(StandardCharsets.UTF_8).length + 1);
connection.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, 52, 0, 1, model, model.length, 1000);

通常真正影响 Device 应用匹配的主要是:Manufacturer + Model

ACCESSORY_START

发送完 Identification 后执行:

int result = connection.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, 53, 0, 0, null, 0, 1000);

Request 53 成功后,对端 Android 会退出当前 USB Device 状态并重新枚举。

因此:

Request 53 成功 ≠ 可以立即 Bulk Transfer

正确流程:Request 53 → close 旧 UsbDeviceConnection → 等待 Device Detached → 等待新的 USB_DEVICE_ATTACHED → 获取新的 UsbDevice

AOA VID/PID

Device 进入 Accessory Mode 后,Host 通常会看到:

VID = 0x18D1

常见 PID:

0x2D00:Accessory

0x2D01:Accessory + ADB

Host 判断:

boolean aoa = device.getVendorId() == 0x18D1 && (device.getProductId() == 0x2D00 || device.getProductId() == 0x2D01);

Request 53 前的 UsbDevice 和重新枚举后的 UsbDevice 不是同一个连接状态,旧 Connection、Interface、Endpoint 都不能继续使用。

Host USB Permission

Host 操作 UsbDevice 前需要:

boolean granted = usbManager.hasPermission(device);

没有权限:

PendingIntent intent = PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION).setPackage(context.getPackageName()), PendingIntent.FLAG_IMMUTABLE);
usbManager.requestPermission(device, intent);

有权限后:

UsbDeviceConnection connection = usbManager.openDevice(device);

Device 重新枚举成 AOA Device 后,需要针对新的 UsbDevice 再检查一次 Permission。

Host 查找 AOA Interface

AOA 数据 Interface 应同时包含 Bulk IN + Bulk OUT。不要默认 getInterface(0) 永远正确,尤其 2D01 还可能同时存在 ADB Interface。

核心遍历逻辑:

for (int i = 0; i < device.getInterfaceCount(); i++) { UsbInterface itf = device.getInterface(i); boolean in = false, out = false; for (int j = 0; j < itf.getEndpointCount(); j++) { UsbEndpoint ep = itf.getEndpoint(j); if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK && ep.getDirection() == UsbConstants.USB_DIR_IN) in = true; if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK && ep.getDirection() == UsbConstants.USB_DIR_OUT) out = true; } if (in && out) { accessoryInterface = itf; break; } }

找到后:

connection.claimInterface(accessoryInterface, true);

Host 查找 Endpoint

for (int i = 0; i < accessoryInterface.getEndpointCount(); i++) { UsbEndpoint ep = accessoryInterface.getEndpoint(i); if (ep.getType() != UsbConstants.USB_ENDPOINT_XFER_BULK) continue; if (ep.getDirection() == UsbConstants.USB_DIR_IN) endpointIn = ep; else if (ep.getDirection() == UsbConstants.USB_DIR_OUT) endpointOut = ep; }

方向始终站在 Host 角度:Bulk OUT:Host → DeviceBulk IN:Device → Host

Host 发送数据

Host → Device:

byte[] data = {(byte) 0xAA, 0x55, 0x01, 0x02};
int length = connection.bulkTransfer(endpointOut, data, data.length, 1000);

length >= 0 表示 USB Transfer 成功,不代表 Device 业务逻辑已经处理完成。

Host 接收数据

Device → Host:

byte[] buffer = new byte[16384];
int length = connection.bulkTransfer(endpointIn, buffer, buffer.length, 1000);
byte[] data = length > 0 ? Arrays.copyOf(buffer, length) : null;

持续接收应该放在工作线程中,避免 Main Thread 阻塞。

Device Manifest

Device Android 声明:

<uses-feature android:name="android.hardware.usb.accessory" android:required="true" />

需要自动匹配 Accessory 时:

<intent-filter>
    <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
</intent-filter>
<meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" android:resource="@xml/accessory_filter" />

accessory_filter.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-accessory manufacturer="AndroidHost" model="VehicleHost" />
</resources>

这里必须与 Host Request 52 发送的:

Manufacturer = AndroidHost

Model = VehicleHost

保持一致。

Device 获取 UsbAccessory

UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
UsbAccessory[] accessories = usbManager.getAccessoryList();
UsbAccessory accessory = accessories != null && accessories.length > 0 ? accessories[0] : null;

可以读取:

getManufacturer() + getModel() + getDescription() + getVersion() + getUri() + getSerial()

这些内容来自 Host 的 Request 52。

Device Permission

boolean granted = usbManager.hasPermission(accessory);

没有权限:

PendingIntent intent = PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION).setPackage(context.getPackageName()), PendingIntent.FLAG_IMMUTABLE);
usbManager.requestPermission(accessory, intent);

需要区分:Host 使用 UsbDevice PermissionDevice 使用 UsbAccessory Permission

Device 打开 Accessory

ParcelFileDescriptor descriptor = usbManager.openAccessory(accessory);
FileDescriptor fd = descriptor.getFileDescriptor();
FileInputStream inputStream = new FileInputStream(fd);
FileOutputStream outputStream = new FileOutputStream(fd);

Device 不需要操作:UsbInterface + UsbEndpoint + UsbDeviceConnection + bulkTransfer()

Framework 已经把 Accessory 数据通道封装成 File Descriptor。

Device 接收 Host 数据

byte[] buffer = new byte[16384];
int length = inputStream.read(buffer);
byte[] data = length > 0 ? Arrays.copyOf(buffer, length) : null;

对应关系:

Host bulkTransfer(Bulk OUT) → Device FileInputStream.read()

Device 发送数据

byte[] data = {0x11, 0x22, 0x33, 0x44};
outputStream.write(data);
outputStream.flush();

对应关系:

Device FileOutputStream.write() → Host bulkTransfer(Bulk IN)

USB 断开

发生 USB Disconnect 后,两端旧对象都失效。

Host 重新获取:UsbDevice + UsbDeviceConnection + UsbInterface + UsbEndpoint

Device 重新获取:UsbAccessory + ParcelFileDescriptor + FileInputStream + FileOutputStream

不要在重新枚举后继续复用旧对象。