Android Open Accessory (AOA) 协议完全解析
目录
- AOA协议概述
- [核心概念:Host Mode与Accessory Mode](#核心概念:Host Mode与Accessory Mode)
- 两种模式核心差异速查
- AOA版本演进
- 协议握手流程详解
- [代码实践:Host Mode完整示例](#代码实践:Host Mode完整示例)
- [代码实践:Accessory Mode完整示例](#代码实践:Accessory Mode完整示例)
- 使用场景总结
一、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);
}
两种权限获取方式:
- Intent Filter方式 :在Manifest中声明
USB_ACCESSORY_ATTACHED的intent-filter,用户连接配件时会被询问是否允许应用自动启动------勾选"默认使用"后可免去每次授权 - 动态请求方式 :在代码中通过
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方案) |