Android Open Accessory (AOA) 协议完全解析

Android Open Accessory (AOA) 协议完全解析

目录

  1. AOA协议概述
  2. [核心概念:Host Mode与Accessory Mode](#核心概念:Host Mode与Accessory Mode)
  3. 两种模式核心差异速查
  4. AOA版本演进
  5. 协议握手流程详解
  6. [代码实践:Host Mode完整示例](#代码实践:Host Mode完整示例)
  7. [代码实践:Accessory Mode完整示例](#代码实践:Accessory Mode完整示例)
  8. 使用场景总结

一、AOA协议概述

Android Open Accessory (AOA) 协议是Google定义的一套标准,允许外部USB硬件(称为Android USB配件)与Android设备进行交互。该协议的核心设计目标,是解决早期Android设备只能作为USB从设备、无法主动连接外部USB设备的限制。

为什么需要AOA?

在AOA出现之前,Android设备通过USB连接时通常扮演"从设备"角色(如连接到电脑时作为存储设备)。这意味着Android设备无法主动控制连接它的U盘、键盘、游戏手柄等外设------因为控制这些外设需要设备具备"USB主机"能力。而很多Android设备并不支持USB Host模式。

AOA通过一种巧妙的角色反转机制绕过了这个限制:让外部配件充当USB主机,Android设备充当USB从机。这种模式下,配件负责供电和管理总线,Android设备则作为"配件"响应通信。

关键特性

  • 配件必须提供500mA、5V的充电电力
  • 配件模式依赖设备硬件支持,并非所有Android设备都支持
  • 应用可通过<uses-feature android:name="android.hardware.usb.accessory"/>在Manifest中声明对配件模式的需求

二、核心概念:Host Mode与Accessory Mode

理解AOA协议的关键,在于厘清USB通信中的两个角色:

USB Host Mode(主机模式)

  • 定义:承担总线管理职责,负责枚举设备、分配地址、供电
  • 典型场景:电脑、支持OTG的手机等
  • 在AOA中的角色 :由外部配件承担

USB Accessory Mode(配件模式)

  • 定义:作为被管理的设备,响应主机指令
  • 典型场景:U盘、键盘、鼠标等
  • 在AOA中的角色 :由Android设备承担

AOA通信中的角色定位

以下表格清晰展示了角色在不同阶段的分配:

阶段 USB Host USB Device
初始连接 Android设备(默认) 外部配件(默认)
AOA握手完成后 外部配件 Android设备

AOA协议的本质,就是协调Android设备和外部配件完成这一角色互换。切换完成后,配件获得总线控制权,可以主动向Android设备发送数据或接收来自设备的数据。


三、两种模式核心差异速查

在实际开发中,Host Mode和Accessory Mode在Android API层面的差异如下:

对比维度 Host Mode(主机模式) Accessory Mode(配件模式)
谁当老板 Android设备充当主机 外部配件充当主机
谁供电 Android设备向外设供电 外部配件向Android设备供电
总线控制权 Android设备拥有 外部配件拥有
核心类 UsbDevice UsbAccessory
获取连接列表 UsbManager.getDeviceList() UsbManager.getAccessoryList()
打开连接 UsbManager.openDevice()UsbDeviceConnection UsbManager.openAccessory()ParcelFileDescriptor
数据传输方式 UsbDeviceConnection.bulkTransfer(endpoint, buffer, ...) FileInputStream / FileOutputStream
端点处理 需手动查找 UsbEndpoint(IN/OUT端点) 无需处理端点,直接用流
权限请求 requestPermission(device, pendingIntent) requestPermission(accessory, pendingIntent)
连接广播 ACTION_USB_DEVICE_ATTACHED ACTION_USB_ACCESSORY_ATTACHED
断开广播 ACTION_USB_DEVICE_DETACHED ACTION_USB_ACCESSORY_DETACHED
XML过滤器 <usb-device> 匹配VID/PID或接口 <usb-accessory> 匹配制造商/型号
Manifest声明 <uses-feature android:name="android.hardware.usb.host"/> <uses-feature android:name="android.hardware.usb.accessory"/>

数据流向示意

复制代码
Host Mode (Android是主机):
┌─────────────┐         ┌──────────────┐
│   Android   │ ──供电──→ │    USB外设    │
│   (Host)    │ ←─数据──→ │   (Device)   │
└─────────────┘         └──────────────┘

Accessory Mode (外部配件是主机):
┌─────────────┐         ┌──────────────┐
│   Android   │ ←─供电── │  外部配件      │
│  (Device)   │ ←─数据──→ │   (Host)     │
└─────────────┘         └──────────────┘

四、AOA版本演进

AOA协议有两个主要版本:

AOAv1(基础版)

  • 发布:2011年Google I/O,随Android 3.1 (API Level 12) 引入
  • 兼容性:Android 2.3.4 (API Level 10) 及以上可通过Add-On Library支持
  • 功能:支持通用配件通信和ADB调试
  • 典型场景:自定义数据传输、设备控制(机器人、传感器等)

AOAv2(增强版)

  • 发布:2012年Google I/O,随Android 4.1 (API Level 16) 引入
  • 新增功能
    • 音频流:配件可作为音频接收端,接收Android设备的音频输出
    • HID(人机接口设备):配件可模拟键盘、鼠标、游戏手柄等输入设备
  • 典型场景:音频底座、游戏外设连接

五、协议握手流程详解

这是AOA协议最核心的部分------配件如何将Android设备从普通模式切换至配件模式。以下流程参考AOA 1.0规范:

第1步:检测设备连接

配件应持续检查是否有Android设备连接。当设备连接时,配件通过读取USB设备描述符获取设备的VID(Vendor ID)和PID(Product ID)。

第2步:判断设备状态

连接时设备可能处于三种状态之一:

状态 VID PID 说明
已处于配件模式 0x18D1 (Google) 0x2D00 或 0x2D01 可直接通信
支持但未切换 设备制造商VID 设备制造商PID 需要触发切换
不支持配件模式 设备制造商VID 设备制造商PID 无法使用AOA

注意0x2D00用于标准配件模式,0x2D01额外支持ADB调试接口。

第3步:查询协议版本(请求码51)

如果设备未处于配件模式,配件发送请求码51查询设备是否支持AOA协议:

复制代码
requestType:    USB_DIR_IN | USB_TYPE_VENDOR
request:        51
value:          0
index:          0
data:           协议版本号(16位小端)

设备若支持,会返回非零版本号(1表示AOAv1,2表示AOAv2)。

第4步:发送身份标识(请求码52)

配件发送请求码52,将自身标识信息传给Android设备。这些信息用于系统匹配对应的应用程序:

String ID 内容 是否必须
0 制造商名称 (Manufacturer) 必须
1 型号名称 (Model) 必须
2 描述 (Description) 可选
3 版本 (Version) 可选
4 URI (指向APK的链接) 可选
5 序列号 (Serial Number) 可选

每个字符串最大256字节(含结束符\0)。

复制代码
requestType:    USB_DIR_OUT | USB_TYPE_VENDOR
request:        52
value:          0
index:          String ID (0-5)
data:           以\0结尾的UTF-8字符串

第5步:启动配件模式(请求码53)

最后,配件发送请求码53,指示设备切换到配件模式:

复制代码
requestType:    USB_DIR_OUT | USB_TYPE_VENDOR
request:        53
value:          0
index:          0
data:           none

发送此请求后,设备会重新枚举(re-enumerate)------相当于USB重新连接,但此时设备将以Google的VID(0x18D1)和配件模式的PID重新出现在总线上。

第6步:建立通信

设备成功进入配件模式后,配件通过查询设备的接口描述符获取批量传输端点(Bulk Endpoints):

  • PID 0x2D00:1个接口,包含2个批量端点(IN + OUT)
  • PID 0x2D01:2个接口,每个接口2个批量端点(第1接口用于数据,第2接口用于ADB)

配件使用SET_CONFIGURATION(0x09)将设备配置设为1,然后即可通过批量端点收发数据。


六、代码实践:Host Mode完整示例

以下代码演示在Android应用侧通过UsbManager主机模式(Host Mode) 连接自定义USB设备。

6.1 Manifest配置

xml 复制代码
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.usbhostdemo">

    <!-- 声明使用USB Host功能 -->
    <uses-feature 
        android:name="android.hardware.usb.host" 
        android:required="true" />

    <application>
        <activity android:name=".HostModeActivity"
            android:launchMode="singleTop">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <!-- 设备插入时自动启动 -->
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
            </intent-filter>

            <meta-data
                android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
                android:resource="@xml/device_filter" />
        </activity>
    </application>
</manifest>

res/xml/device_filter.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 匹配特定VID/PID的设备 -->
    <usb-device 
        vendor-id="1234" 
        product-id="5678" />
</resources>

6.2 Host Mode核心通信代码

java 复制代码
package com.example.usbhostdemo;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;

import java.util.HashMap;

public class HostModeActivity extends Activity {

    private static final String TAG = "HostMode";
    private static final String ACTION_USB_PERMISSION = 
        "com.example.usbhostdemo.USB_PERMISSION";
    private static final int TIMEOUT = 1000;

    private UsbManager mUsbManager;
    private UsbDevice mUsbDevice;
    private UsbDeviceConnection mConnection;
    private UsbInterface mInterface;
    private UsbEndpoint mEndpointIn;
    private UsbEndpoint mEndpointOut;

    private TextView mStatusTextView;
    private TextView mDataTextView;
    private Handler mMainHandler = new Handler(Looper.getMainLooper());
    private volatile boolean mIsReading = false;
    private Thread mReadThread;

    // 权限请求广播接收器
    private final BroadcastReceiver mPermissionReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (ACTION_USB_PERMISSION.equals(intent.getAction())) {
                UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                    if (device != null) openDevice(device);
                } else {
                    Toast.makeText(HostModeActivity.this, "USB权限被拒绝", Toast.LENGTH_SHORT).show();
                }
            }
        }
    };

    // 设备插拔监听
    private final BroadcastReceiver mDeviceReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
                UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                if (device != null) requestDevicePermission(device);
            } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
                UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                if (device != null && device.equals(mUsbDevice)) {
                    closeDevice();
                }
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_host_mode);

        mStatusTextView = findViewById(R.id.status_text);
        mDataTextView = findViewById(R.id.data_text);
        mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE);

        IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_USB_PERMISSION);
        filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
        filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
        registerReceiver(mPermissionReceiver, filter);
        registerReceiver(mDeviceReceiver, filter);
    }

    @Override
    protected void onResume() {
        super.onResume();
        HashMap<String, UsbDevice> deviceList = mUsbManager.getDeviceList();
        if (!deviceList.isEmpty()) {
            UsbDevice device = deviceList.values().iterator().next();
            if (mUsbManager.hasPermission(device)) {
                openDevice(device);
            } else {
                requestDevicePermission(device);
            }
        }
    }

    private void requestDevicePermission(UsbDevice device) {
        PendingIntent pi = PendingIntent.getBroadcast(
            this, 0, new Intent(ACTION_USB_PERMISSION), 
            PendingIntent.FLAG_IMMUTABLE
        );
        mUsbManager.requestPermission(device, pi);
    }

    /**
     * Host Mode关键方法:通过UsbDevice打开连接
     */
    private void openDevice(UsbDevice device) {
        mUsbDevice = device;
        mInterface = device.getInterface(0);

        // 查找批量传输端点
        for (int i = 0; i < mInterface.getEndpointCount(); i++) {
            UsbEndpoint ep = mInterface.getEndpoint(i);
            if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) {
                if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
                    mEndpointIn = ep;
                } else {
                    mEndpointOut = ep;
                }
            }
        }

        // Host Mode核心:通过openDevice()获取UsbDeviceConnection
        mConnection = mUsbManager.openDevice(device);
        if (mConnection == null) {
            mStatusTextView.setText("打开设备失败");
            return;
        }

        // 声明接口(占用设备)
        if (!mConnection.claimInterface(mInterface, true)) {
            mConnection.close();
            mConnection = null;
            mStatusTextView.setText("声明接口失败");
            return;
        }

        mStatusTextView.setText("Host模式已连接");
        startReading();
        sendCommand(new byte[]{0x01, 0x00});
    }

    /**
     * Host Mode发送数据:使用bulkTransfer
     */
    public void sendCommand(byte[] data) {
        if (mConnection == null || mEndpointOut == null) return;
        new Thread(() -> {
            int transferred = mConnection.bulkTransfer(
                mEndpointOut, data, data.length, TIMEOUT
            );
            mMainHandler.post(() -> 
                mStatusTextView.setText("发送: " + transferred + "字节")
            );
        }).start();
    }

    /**
     * Host Mode接收数据:使用bulkTransfer轮询
     */
    private void startReading() {
        mIsReading = true;
        mReadThread = new Thread(() -> {
            byte[] buffer = new byte[64];
            while (mIsReading && mConnection != null && mEndpointIn != null) {
                int len = mConnection.bulkTransfer(
                    mEndpointIn, buffer, buffer.length, TIMEOUT
                );
                if (len > 0) {
                    final byte[] data = new byte[len];
                    System.arraycopy(buffer, 0, data, 0, len);
                    mMainHandler.post(() -> 
                        mDataTextView.setText("接收: " + bytesToHex(data))
                    );
                }
            }
        });
        mReadThread.start();
    }

    private void closeDevice() {
        mIsReading = false;
        if (mConnection != null) {
            mConnection.releaseInterface(mInterface);
            mConnection.close();
            mConnection = null;
        }
        mStatusTextView.setText("已断开");
    }

    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) sb.append(String.format("%02X ", b));
        return sb.toString().trim();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mPermissionReceiver);
        unregisterReceiver(mDeviceReceiver);
    }
}

七、代码实践:Accessory Mode完整示例

以下代码演示在Android应用侧如何通过UsbManager配件模式(Accessory Mode) 连接外部配件。

7.1 Manifest配置

AndroidManifest.xml中声明配件模式支持:

xml 复制代码
<uses-feature android:name="android.hardware.usb.accessory" />

<application>
    <activity android:name=".MainActivity">
        <!-- 配件连接时自动启动Activity -->
        <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" />
    </activity>
</application>

res/xml/accessory_filter.xml内容:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-accessory 
        manufacturer="Google, Inc." 
        model="DemoKit" 
        version="1.0" />
</resources>

7.2 权限管理

AOA使用独立于普通USB设备的权限机制,通过UsbManager.requestPermission()申请:

java 复制代码
private static final String ACTION_USB_PERMISSION = 
    "com.example.USB_PERMISSION";

private void requestAccessoryPermission(UsbAccessory accessory) {
    PendingIntent permissionIntent = PendingIntent.getBroadcast(
        this, 0, new Intent(ACTION_USB_PERMISSION), 
        PendingIntent.FLAG_IMMUTABLE
    );
    mUsbManager.requestPermission(accessory, permissionIntent);
}

两种权限获取方式

  1. Intent Filter方式 :在Manifest中声明USB_ACCESSORY_ATTACHED的intent-filter,用户连接配件时会被询问是否允许应用自动启动------勾选"默认使用"后可免去每次授权
  2. 动态请求方式 :在代码中通过requestPermission()主动申请

7.3 打开配件通信

获取权限后,通过UsbManager.openAccessory()获取ParcelFileDescriptor,进而获得输入/输出流:

java 复制代码
private UsbManager mUsbManager;
private UsbAccessory mAccessory;
private ParcelFileDescriptor mFileDescriptor;
private FileInputStream mInputStream;
private FileOutputStream mOutputStream;

private void openAccessory(UsbAccessory accessory) {
    // Accessory Mode核心:通过openAccessory()获取ParcelFileDescriptor
    mFileDescriptor = mUsbManager.openAccessory(accessory);
    if (mFileDescriptor != null) {
        mAccessory = accessory;
        FileDescriptor fd = mFileDescriptor.getFileDescriptor();
        mInputStream = new FileInputStream(fd);
        mOutputStream = new FileOutputStream(fd);
        
        // 启动后台线程读取数据
        new Thread(() -> {
            byte[] buffer = new byte[16384];
            int len;
            try {
                while ((len = mInputStream.read(buffer)) >= 0) {
                    // 处理接收到的数据
                }
            } catch (IOException e) {
                // 处理异常
            }
        }).start();
    }
}

7.4 发送与接收数据

发送数据

java 复制代码
public void sendCommand(String command) {
    if (mOutputStream != null) {
        try {
            mOutputStream.write(command.getBytes());
            mOutputStream.flush();
        } catch (IOException e) {
            // 处理异常
        }
    }
}

接收数据(在后台线程中轮询):

java 复制代码
private void startReading() {
    new Thread(() -> {
        byte[] buffer = new byte[1024];
        int len;
        try {
            while (true) {
                len = mInputStream.read(buffer);
                if (len > 0) {
                    String received = new String(buffer, 0, len);
                    // 通过Handler发送到UI线程处理
                    mHandler.obtainMessage(MSG_DATA, received).sendToTarget();
                }
            }
        } catch (IOException e) {
            // 连接断开
        }
    }).start();
}

7.5 处理断开事件

通过广播接收器监听断开事件:

java 复制代码
private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) {
            UsbAccessory accessory = intent.getParcelableExtra(
                UsbManager.EXTRA_ACCESSORY
            );
            if (accessory != null && accessory.equals(mAccessory)) {
                closeAccessory();
            }
        }
    }
};

7.6 完整生命周期管理

在Activity生命周期中合理管理连接资源:

java 复制代码
@Override
protected void onResume() {
    super.onResume();
    UsbAccessory[] accessories = mUsbManager.getAccessoryList();
    if (accessories != null && accessories.length > 0) {
        UsbAccessory accessory = accessories[0];
        if (mUsbManager.hasPermission(accessory)) {
            openAccessory(accessory);
        } else {
            requestAccessoryPermission(accessory);
        }
    }
}

@Override
protected void onPause() {
    super.onPause();
    closeAccessory();
}

private void closeAccessory() {
    try {
        if (mFileDescriptor != null) {
            mFileDescriptor.close();
        }
    } catch (IOException e) {
        // 处理异常
    } finally {
        mFileDescriptor = null;
        mAccessory = null;
        mInputStream = null;
        mOutputStream = null;
    }
}

八、使用场景总结

基于以上技术分析,AOA协议在实际中的应用场景可归纳为:

场景类型 适用模式 适用版本 典型应用
自定义USB外设通信 Host Mode --- 传感器数据采集、LED控制器、工业设备
标准USB外设 Host Mode --- U盘读写、键盘鼠标、游戏手柄
自定义数据通信 Accessory Mode AOAv1 机器人控制、传感器数据采集
ADB调试 Accessory Mode AOAv1 开发调试、自动化测试
音频外设 Accessory Mode AOAv2 车载音响底座、智能音箱扩展坞
HID输入设备 Accessory Mode AOAv2 游戏手柄、键盘、鼠标、遥控器
车载互联 Accessory Mode AOAv2 车载信息娱乐系统(如SmartDeviceLink方案)
相关推荐
我命由我123453 小时前
Android 开发 - 广播组件(标准广播、有序广播、静态注册广播、分钟到达广播、网络变更广播...)
android·java·开发语言·网络·java-ee·android studio·android-studio
A富得流油的咸鸭蛋3 小时前
安卓鸿蒙面试
android·面试·harmonyos
Coffeeee3 小时前
AGP9.0的主要变更项,给Gradle来一次大变样
android·前端·gradle
小孔龙4 小时前
Android GPU 渲染管线:一帧画面如何走上屏幕
android·性能优化·gpu
米码收割机4 小时前
【Android】Android 书澜图书馆信息管理系统(源码+文档)【独一无二】
android
恋猫de小郭4 小时前
KotlinLLM 开源 ,一个可以在运行时自己生成永久 Kotlin 代码的 Agent 库
android·前端·人工智能
游戏开发爱好者84 小时前
iOS应用加固方案全解析:源码加固与IPA在线加固对比
android·macos·ios·小程序·uni-app·cocoa·iphone
大鹏说大话5 小时前
PHP 异步处理方案:队列使用场景与 RabbitMQ 实操
android
molihuan5 小时前
最新 Paddle-Lite Android平台编译
android·ocr·paddle·推理·端侧·paddle lite