3. Android USB主机模式开发:USB Host API介绍与设备发现

好,咱们今天来聊聊Android USB主机模式开发。说实话,这部分内容在嵌入式开发里算是比较硬核的。我当年第一次接触USB Host API时,也被那一堆UsbManager、UsbDevice、UsbInterface、UsbEndpoint搞得有点懵。但别怕,捋顺了其实就那么回事。

先给你看一张整体流程图,让你心里有个谱:

USB主机模式开发核心流程 1. 获取UsbManager getSystemService() 2. 枚举设备列表 getDeviceList() 3. 请求权限 requestPermission() 4. 通信 open() UsbDevice 内部结构 UsbDevice getDeviceId() / getVendorId() UsbInterface getInterfaceClass() UsbEndpoint getAddress() 一个UsbDevice包含多个UsbInterface 一个UsbInterface包含多个UsbEndpoint UsbEndpoint分为IN(读)和OUT(写)两种方向

3.1 USB Host API 核心类解析

Android的USB Host API其实就四个核心类,我习惯叫它们「四件套」:UsbManager、UsbDevice、UsbInterface、UsbEndpoint。你把这四个搞明白了,剩下的就是拼积木。

3.1.1 UsbManager —— 总管

UsbManager是系统服务,负责管理所有USB设备。获取方式很简单:

UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);

嗯,这里要注意,这个服务在Android 3.1(API 12)之后才引入。我记得有一次在平板上调试,死活拿不到UsbManager,后来才发现那台平板系统版本太老。所以如果你的项目要兼容老设备,得先判断一下。

UsbManager主要干三件事:

  • getDeviceList() —— 扫描当前所有已连接的USB设备
  • requestPermission() —— 向用户申请访问某个USB设备的权限
  • openDevice() —— 打开设备,拿到通信用的FileDescriptor

3.1.2 UsbDevice —— 设备本体

UsbDevice代表一个物理USB设备。它本身不负责通信,而是描述设备的信息。常用的方法:

方法 返回值 说明
getDeviceId() int 设备唯一ID,系统分配
getVendorId() int 厂商ID,比如0x18D1是Google
getProductId() int 产品ID,每个厂商自己定义
getDeviceClass() int 设备类,比如大容量存储、HID等
getInterfaceCount() int 该设备包含的接口数量
getInterface(int index) UsbInterface 获取指定索引的接口

说白了,UsbDevice就是设备的「身份证」,告诉你这设备是谁家的、什么型号、有几个接口。

3.1.3 UsbInterface —— 功能单元

一个USB设备可以包含多个接口,每个接口代表一个独立的功能单元。比如一个USB摄像头,可能同时有视频接口和音频接口。

UsbInterface的关键方法:

  • getId() —— 接口ID
  • getInterfaceClass() —— 接口类,比如0x08是大容量存储,0x03是HID
  • getEndpointCount() —— 该接口下的端点数量
  • getEndpoint(int index) —— 获取指定索引的端点
我的经验: 很多初学者一上来就遍历所有接口,其实没必要。你只需要找到你关心的那个接口类就行。比如做USB串口通信,就找接口类为0xFF(厂商自定义)或者0x02(CDC控制)的那个。

3.1.4 UsbEndpoint —— 数据通道

UsbEndpoint是真正收发数据的地方。每个端点都有一个方向:

  • USB_DIR_IN —— 设备到主机(读数据)
  • USB_DIR_OUT —— 主机到设备(写数据)

还有传输类型:

类型 常量 用途
控制传输 USB_ENDPOINT_XFER_CONTROL 设备枚举、配置查询
批量传输 USB_ENDPOINT_XFER_BULK 大数据量传输,如U盘、串口
中断传输 USB_ENDPOINT_XFER_INT 小数据量、实时性要求高,如键盘、鼠标
等时传输 USB_ENDPOINT_XFER_ISO 音视频流,Android不支持

你想想看,Android只支持前三种,等时传输被砍掉了。所以做USB音频采集的话,这条路走不通,得另想办法。

3.2 权限管理 —— 绕不开的坎

Android对USB设备的访问有严格的权限控制。说白了,你不能偷偷摸摸地访问用户的USB设备,必须弹窗让用户同意。

3.2.1 动态申请权限

最常用的方式是动态申请:

UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
UsbDevice device = ... // 从getDeviceList()获取

PendingIntent permissionIntent = PendingIntent.getBroadcast(
    this, 0, new Intent(ACTION_USB_PERMISSION), 
    PendingIntent.FLAG_IMMUTABLE);

usbManager.requestPermission(device, permissionIntent);

这里有个坑,我踩过好几次。PendingIntent的FLAG_IMMUTABLE是Android 12之后强制要求的。如果你的targetSdkVersion比较高,不加这个flag会直接崩溃。嗯,兼容性这东西,永远是个麻烦事。

3.2.2 广播监听权限结果

用户点击弹窗后,结果通过广播返回:

private final BroadcastReceiver usbReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (ACTION_USB_PERMISSION.equals(action)) {
            synchronized (this) {
                UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                    // 用户同意了,可以开始通信
                    if (device != null) {
                        // 调用openDevice()
                    }
                } else {
                    // 用户拒绝了
                    Log.w(TAG, "用户拒绝了USB设备访问权限");
                }
            }
        }
    }
};

// 注册广播
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
registerReceiver(usbReceiver, filter, Context.RECEIVER_EXPORTED);
注意: Android 13之后,注册广播需要指定RECEIVER_EXPORTED或RECEIVER_NOT_EXPORTED。如果你不指定,系统会抛异常。我曾经在升级targetSdkVersion时被这个坑了一下午。

3.2.3 另一种方式:设备过滤文件

如果你不想每次都弹窗,可以在AndroidManifest.xml里声明设备过滤:

<activity ...>
    <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>

然后在res/xml/device_filter.xml里写:

<resources>
    <usb-device vendor-id="1027" product-id="24577" />
</resources>

这种方式的好处是,插上指定设备后,系统会自动弹出你的应用。但说实话,我一般不用这种方式,因为太死板了。万一用户换了个不同PID的设备,你的应用就识别不了了。

3.3 设备发现与枚举流程

好,现在我们把所有知识点串起来,写一个完整的设备发现流程。

3.3.1 扫描已连接的设备

UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();

if (deviceList.isEmpty()) {
    Log.i(TAG, "没有检测到USB设备");
    return;
}

for (UsbDevice device : deviceList.values()) {
    Log.i(TAG, "发现设备: VID=" + device.getVendorId() 
          + " PID=" + device.getProductId());
    
    // 遍历接口
    for (int i = 0; i < device.getInterfaceCount(); i++) {
        UsbInterface usbInterface = device.getInterface(i);
        Log.i(TAG, "  接口" + i + ": 类=" + usbInterface.getInterfaceClass());
        
        // 遍历端点
        for (int j = 0; j < usbInterface.getEndpointCount(); j++) {
            UsbEndpoint endpoint = usbInterface.getEndpoint(j);
            Log.i(TAG, "    端点" + j + ": 方向=" 
                  + (endpoint.getDirection() == UsbConstants.USB_DIR_IN ? "IN" : "OUT")
                  + " 类型=" + endpoint.getType());
        }
    }
}

3.3.2 监听设备插拔

除了主动扫描,你还可以监听设备插拔事件:

// 设备插入
IntentFilter filterAttached = new IntentFilter(
    UsbManager.ACTION_USB_DEVICE_ATTACHED);
registerReceiver(usbAttachedReceiver, filterAttached);

// 设备拔出
IntentFilter filterDetached = new IntentFilter(
    UsbManager.ACTION_USB_DEVICE_DETACHED);
registerReceiver(usbDetachedReceiver, filterDetached);

我个人习惯把设备插拔监听放在onResume里注册,onPause里注销。这样既不会漏掉事件,也不会造成内存泄漏。

3.3.3 完整的枚举流程

把上面所有东西整合起来,一个典型的设备发现流程是这样的:

  1. 获取UsbManager —— 从系统服务拿到总管对象
  2. 调用getDeviceList() —— 拿到当前所有设备的Map
  3. 遍历设备 —— 根据VID/PID找到你关心的设备
  4. 请求权限 —— 弹窗让用户确认
  5. 权限回调 —— 在广播接收器里处理用户选择
  6. 打开设备 —— 调用openDevice()获取UsbDeviceConnection
  7. 声明接口 —— 调用claimInterface()占用接口
  8. 开始通信 —— 通过端点收发数据

核心要点:

  • UsbManager是入口,所有操作都从它开始
  • UsbDevice是描述信息,不负责通信
  • UsbInterface是功能单元,一个设备可以有多个
  • UsbEndpoint是数据通道,分IN和OUT
  • 权限必须动态申请,不能绕过
  • 设备插拔可以广播监听

嗯,到这里USB主机模式的核心API和流程就讲完了。你可能会觉得代码量有点多,但别急,下一节我们会用一个真实的串口通信案例,把这些代码串起来跑一遍。到时候你就知道,其实就那么几行核心代码在反复用。

一个小建议: 刚开始做USB开发时,建议先拿一个简单的设备练手,比如USB转串口模块(CH340或CP2102)。这种设备结构简单,接口和端点都很少,调试起来不费劲。我当年就是拿CH340入的门,三天就调通了收发数据。
公众号:蓝海资料掘金营,微信deep3321