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方案)
相关推荐
古法安卓2 小时前
Android-SELinux 策略调试实战:从 AVC 日志到策略修复
android·java·android studio
Dovis(誓平步青云)3 小时前
DevEco Studio 6.1.1 Windows 安装实录:从下载校验到首次启动
android·开发语言·数据库·人工智能·windows·harmonyos
Zender Han3 小时前
Flutter 自适应(Adaptive)与响应式(Responsive)设计实践:官方推荐方案详解
android·flutter·ios
我命由我123454 小时前
Android 开发问题:TopAppBar 和 topAppBarColors API is experimental...
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
造火箭4 小时前
Android UI自动化测试可行性评估SKILL
android·功能测试·ui
hunterandroid6 小时前
[Android 从零到一] ViewPager2 与 Fragment 生命周期协同:从预加载到状态一致性
android·前端
mmsx6 小时前
一个黑边 Bug 修了两版:自己算矩阵直接黑屏,借库重建只用了一行 setZoom
android·前端
枢影Kernel6 小时前
Android CLI 与 Android Skills 最佳实践:把 AI Agent 接入可验证的 Android 开发流程
android
Kapaseker7 小时前
没想到吧!Skill 也可以测试 — 小白都看得懂的 Skill 教程
android·人工智能·kotlin
我命由我123457 小时前
Android 开发问题:使用 AndroidTreeView 时,自定义视图无法撑满父容器
android·java·java-ee·kotlin·android studio·android-studio·android runtime