深入浅出 Android AOA 协议:通信流程与设备切换附着机制解析

深入浅出 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(如 0x2D000x2D01)来拦截重连设备。确认身份后,遍历底层的 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)的方式,将其转换为我们熟悉的 FileInputStreamFileOutputStream 进行数据读写。

关键代码实现:

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 等车机互联协议的基石。

相关推荐
恋猫de小郭1 小时前
Amper 正式转正 Kotlin Toolchain ,Gradle 未来何去何从
android·前端·flutter
敲代码的彭于晏1 小时前
Bean 生命周期完全图解:前端同学也能看懂的 Spring 核心机制
java·前端·后端
IT_陈寒1 小时前
Redis内存飙升的锅,原来是我没搞懂这个过期策略
前端·人工智能·后端
云浪1 小时前
前端二进制数组完全指南:ArrayBuffer、TypedArray、DataView 一次讲透
前端·javascript
张风捷特烈1 小时前
Flutter 类库大揭秘#02 | path_provider 各平台实现
前端·flutter
铁皮饭盒2 小时前
26年bunjs, elysia+pg一把梭, redis都省了
前端·javascript·后端
lichenyang45315 小时前
Docker 学习笔记(一):为什么需要镜像、容器和仓库?
前端
kyriewen15 小时前
别再对着 TypeScript 报错发呆了:我把 10 个最常见的红色波浪线翻译成了人话
前端·javascript·typescript