3. Android USB主机模式开发:USB Host API介绍与设备发现
好,咱们今天来聊聊Android USB主机模式开发。说实话,这部分内容在嵌入式开发里算是比较硬核的。我当年第一次接触USB Host API时,也被那一堆UsbManager、UsbDevice、UsbInterface、UsbEndpoint搞得有点懵。但别怕,捋顺了其实就那么回事。
先给你看一张整体流程图,让你心里有个谱:
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) —— 获取指定索引的端点
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);
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 完整的枚举流程
把上面所有东西整合起来,一个典型的设备发现流程是这样的:
- 获取UsbManager —— 从系统服务拿到总管对象
- 调用getDeviceList() —— 拿到当前所有设备的Map
- 遍历设备 —— 根据VID/PID找到你关心的设备
- 请求权限 —— 弹窗让用户确认
- 权限回调 —— 在广播接收器里处理用户选择
- 打开设备 —— 调用openDevice()获取UsbDeviceConnection
- 声明接口 —— 调用claimInterface()占用接口
- 开始通信 —— 通过端点收发数据
核心要点:
- UsbManager是入口,所有操作都从它开始
- UsbDevice是描述信息,不负责通信
- UsbInterface是功能单元,一个设备可以有多个
- UsbEndpoint是数据通道,分IN和OUT
- 权限必须动态申请,不能绕过
- 设备插拔可以广播监听
嗯,到这里USB主机模式的核心API和流程就讲完了。你可能会觉得代码量有点多,但别急,下一节我们会用一个真实的串口通信案例,把这些代码串起来跑一遍。到时候你就知道,其实就那么几行核心代码在反复用。