ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android UsbManager封装:枚举、权限与bulk读写实战

Android UsbManager封装:枚举、权限与bulk读写实战 简介面向希望在Android应用中接入USB外设、开发OTG传输或调试USB通信的开发者此压缩包提供UsbManager核心类的Java源码用于理解Android USB框架中的主机模式与设备模式。包内仅此1个Java文件压缩后大小2KB轻量无依赖便于直接阅读或拷贝至工程参考。该资源已有146人学习浏览适合初学USBManager或需要快速查阅类用法的读者。源码覆盖USBManager的主要功能主机模式下可枚举UsbDevice并获取vendor ID、product ID等设备信息选择Configuration和Interface后打开UsbDeviceConnection进行端点读写设备模式下可通过OTG实现角色切换并借助权限请求机制保护用户授权。同时还可结合BroadcastReceiver监听USB设备插入与拔出理解热插拔事件处理流程。无论是连接打印机、摄像头、存储设备还是处理MIDI音频设备都能从中找到对应的接口思路。对于编写自定义USB驱动或适配复合设备的开发者这份源码能提供清晰的调用框架和关键API参考有效缩短USB功能接入的排错时间。1. 一个 UsbManager 类能省下多少底层功夫很多 Android 板卡项目里业务代码稳定跑了大半年最后卡在 USB 外设的插拔和权限上。网上流传过不少类似 UsbManager.rar_The Class_USBManager 的资源包压缩包里真正有价值的东西通常不是那个 Class 文件本身而是其中用 USBManager 命名的这层封装。它把系统 UsbManager 的枚举、授权、claimInterface 和 bulk 读写收成几个可复用的方法开发时就不用每换一个外设就重写一遍通信代码。这篇按这种封装思路来讲先划清枚举、权限和传输三层边界再给一版可落地的 Java 类最后列出实际调试时最容易出现的失败点和调参建议。适配场景是 Android USB Host、Kiosk 外设以及带 Linux 底层的嵌入式上位机。2. UsbManager 类管理的边界枚举、权限与传输模型动手做类之前先把系统 UsbManager 到底管什么、不管什么理清。系统层的 UsbManager 是设备元数据的窗口也是通信通道的提供者但它不负责解析任何具体协议。一个封装良好的 Class_USBManager应该把「找设备、拿权限、开通道、收发数据」这几件事收敛成稳定接口而把 HID 报表、CDC 串口、Mass Storage 命令字这些协议细节留在外层业务里。2.1 枚举四层结构先于业务逻辑一台 USB 设备从总线角度看是四层Device、Configuration、Interface、Endpoint。UsbManager.getDeviceList()拿到的只是第一层设备对象真正决定能不能通信的是 Interface 层。Linux 下用lsusb -v能看到bInterfaceClass、bInterfaceSubClass、bInterfaceProtocol这组描述符Android 侧则通过UsbDevice.getInterface(i)遍历同一棵树。常见做法是在类里先按目标设备的 interfaceClass 过滤再回填 VID/PID。因为很多国产外设换 firmware 后 PID 会变而接口类0xFFvendor-specific或 CDC 数据类通常不变for (int i 0; i device.getInterfaceCount(); i) { UsbInterface intf device.getInterface(i); if (intf.getInterfaceClass() UsbConstants.USB_CLASS_CDC_DATA) { targetInterface intf; break; } }这段代码的逻辑是遍历设备的所有接口命中 CDC 数据接口后直接停。之所以用getInterfaceClass()而不是只认 VID/PID是因为同一条产线上的扫描枪、标签打印机可能共用同一套 SoC描述符树相似但接口类才是业务功能的稳定标识。值得注意这里说的「Class」是指 USB 描述符里的 bInterfaceClass和标题里 Class_USBManager 那个面向对象意义上的 Class 是两个层次别混。2.2 权限模型可见不等于可操作枚举层面能看到设备不代表能 open。Android 的 UsbManager 在未授权状态下openDevice()会抛 SecurityException已经授权过的设备再次插上系统会直接放行但 App 进程重启后授权信息仍在权限状态却要重新确认。所以管理器类里必须有一个状态位记录「当前设备是否拿到通信权限」。Linux 上等价的问题是 udev 规则和设备节点权限。用 libusb 或直接访问/dev/bus/usb时root 之外的用户如果没有对应 udev ruleopen 会返回 EACCES。App 层的 UsbManager 则可以这样收敛权限逻辑平台授权动作事件来源常见失败AndroidrequestPermission() BroadcastReceiverACTION_USB_DEVICE_DETACHEDSecurityExceptionLinux udev用户组规则netlink ueventopen 返回 EACCESlibusbclaim_interface()hotplug callbackLIBUSB_ERROR_ACCESS封装建议是只暴露boolean ensurePermission(UsbDevice device)内部处理平台差异业务侧不需要关心广播注册在哪一层。后面第四章会展开一个非常典型的广播注册周期问题。2.3 传输模型管理器类实际收发的两个端点USB 传输有 control、bulk、interrupt、isochronous 四种App 层最常用的是 bulk。端点是有方向的IN 端点是设备到主机OUT 端点是主机到设备。一个接口下通常有成对端点但配对关系并不总是一个 IN 一个 OUT 相邻排列所以类内部需要按方向加类型去查找。public int read(UsbEndpoint inEndpoint, byte[] buffer, int timeout) { if (connection null) return -1; return connection.bulkTransfer(inEndpoint, buffer, buffer.length, timeout); }参数timeout单位是毫秒传入0表示无限等待但这在拔插场景里非常危险。read 方法返回负数才是错误返回 0 只是收到零长度包两者语义不同。这个返回值设计是后面所有实战排错的基础它决定了 manager 类对外暴露的是异常还是状态码。个人经验是管理器类一律返回状态码把异常留在类内部转成可读错误信息否则调用侧 try-catch 会写得很难看。3. 从零实现一个可复用的 Class_USBManager这章给一版精简但可运行的实现。类名可以直接叫UsbManager但要注意和系统android.hardware.usb.UsbManager撞名代码里必须用全限定名持有系统实例避免编译期解析混乱。这也是很多从压缩包里直接拷 Class 文件的人遇到的第一个坑。3.1 类骨架宁可多实例不要大单例很多项目习惯把 USB 管理器写成静态单例但如果一个 App 要同时管扫描枪和打印机单例就要维护两个连接状态复杂度立刻上来。更稳的结构是每个业务设备持有一个管理器实例public class UsbManager { private final android.hardware.usb.UsbManager systemManager; private UsbDevice device; private UsbDeviceConnection connection; private UsbInterface activeInterface; public UsbManager(Context context) { systemManager (android.hardware.usb.UsbManager) context.getSystemService(Context.USB_SERVICE); } }构造器里通过getSystemService拿到系统管理器成员变量保存当前设备、连接和已占用的接口。这里没有把 timeout 和 buffer size 写死因为它们和端点强相关应该在拿到 endpoint 后再决定。3.2 枚举与过滤VID/PID/InterfaceClass 三级选择器设备选择是一个三级过滤先按 VID/PID 粗筛再按接口类确认业务能力最后按端点方向确认可通信public UsbDevice findDevice(int targetVid, int targetPid, int targetClass) { HashMapString, UsbDevice devices systemManager.getDeviceList(); for (UsbDevice dev : devices.values()) { if (targetVid ! 0 dev.getVendorId() ! targetVid) continue; if (targetPid ! 0 dev.getProductId() ! targetPid) continue; for (int i 0; i dev.getInterfaceCount(); i) { if (dev.getInterface(i).getInterfaceClass() targetClass) { return dev; } } } return null; }逻辑说明遍历系统返回的设备表VID/PID 为 0 时表示不参与过滤命中指定接口类后直接返回设备。这样设计是因为很多设备类0xFF的 vendor 设备只靠 VID/PID 容易在换固件后失配接口类通常是稳定的。3.3 连接时序授权、openDevice、claimInterface 三步缺一不可拿到设备对象后正确时序是hasPermission-requestPermission-openDevice-claimInterface。授权是异步的所以连接方法只处理已授权状态public boolean connect(UsbDevice target, int interfaceIndex) { if (!systemManager.hasPermission(target)) return false; this.device target; connection systemManager.openDevice(target); if (connection null) return false; activeInterface target.getInterface(interfaceIndex); if (!connection.claimInterface(activeInterface, true)) { connection.close(); connection null; return false; } return true; }参数说明interfaceIndex是设备描述符中的接口下标claimInterface的第二个参数force为 true 时占用该接口。注意openDevice返回 null 不一定代表设备坏了也可能是系统还在为设备分配带宽或权限刚被撤回。connect 返回 false 后调用方应该去检查lastError而不是直接重试。3.4 读写与释放把端点搜索收到类内部调用方不应该关心端点搜索逻辑管理器类要提供按业务语义命名的readData()和writeData()public int writeData(byte[] data, int timeout) { UsbEndpoint out findEndpoint(UsbConstants.USB_DIR_OUT, UsbConstants.USB_ENDPOINT_XFER_BULK); if (out null) return -1; return connection.bulkTransfer(out, data, data.length, timeout); } private UsbEndpoint findEndpoint(int direction, int type) { for (int i 0; i activeInterface.getEndpointCount(); i) { UsbEndpoint ep activeInterface.getEndpoint(i); if (ep.getDirection() direction ep.getType() type) { return ep; } } return null; }逻辑说明write 之前先找 OUT 方向、bulk 类型的端点找不到返回 -1。调用方不需要知道端点是 endpoint 1 还是 endpoint 2。释放顺序则固定为releaseInterface再close中间不能省否则下次连接可能返回 BUSY。3.5 压缩包分发的兼容性别只丢一个 .class 文件标题里的 rar 只是一个分发载体这类资源包常见问题是只放一个编译后的 class。编译产物的兼容性取决于 javac 的-source和-targetAndroid 设备上尤其敏感javac -source 1.8 -target 1.8 UsbManager.java jar cvf UsbManager.jar UsbManager.class参数说明-source 1.8和-target 1.8保证字节码版本是 Java 8如果直接用默认的 JDK 17 编译class 文件版本号是 61放到低版本 Android 运行环境里会直接 NoSuchMethodError。相比打包成 .class源码包反而更好维护分发形式优势主要风险单个 .class体积最小字节码版本不兼容、反编译困难JAR依赖结构清楚内部类、资源文件容易漏源码 构建脚本可改可调试需要对方有编译环境实战中比较稳的做法是源码和构建脚本一起打包并且把依赖的最小 API level 写清楚。4. UsbManager 实战中的 4 个失败现场与参数修正类写好了真正跑起来才会暴露问题。这 4 个失败现场在设备调试群里出现频率最高每个都值得在 manager 类里做防御性处理。4.1 权限广播丢失Receiver 注册周期现象是系统弹窗点了允许但 App 没有任何回调。大多数情况是BroadcastReceiver注册在 Activity 的临时区域而授权弹窗会让 Activity 进入 onPause注册被系统慢慢回收。正确做法是在 Activity onCreate 时注册onDestroy 时反注册并且使用自定义 actionprivate static final String ACTION_USB_PERMISSION com.example.USB_PERMISSION; PendingIntent pi PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_MUTABLE); systemManager.requestPermission(device, pi);参数说明FLAG_MUTABLE是 targetSdk 31 之后的强制要求targetSdk 较低时传0即可。广播接收器里要根据intent.getAction()判断授权事件不要直接拿系统默认的 attach/detach action 来当授权回调。4.2 bulkTransfer 返回 0 或长度不完整很多设备是高频小包模式比如扫码枪按 8 字节一包上报。如果缓冲区只开 64 字节一次 bulk read 可能只取到一包就返回后面数据要再读一轮。这不是错误但容易让上层误判为协议异常。byte[] buffer new byte[endpoint.getMaxPacketSize() * 16]; int total 0; while (total need) { int len connection.bulkTransfer(endpoint, buffer, buffer.length, timeout); if (len 0) break; total len; }逻辑说明缓冲区按maxPacketSize的整数倍分配循环读直到凑足业务需要的字节数或读失败。注意bulkTransfer返回-1才是错误0是空包小于请求长度是短包三种情况要分开处理。4.3 拔插瞬时的异常与返回码USB 设备被拔掉时正在阻塞的 bulkTransfer 不会立刻抛异常而是等超时后返回-1。更隐蔽的是connection对象本身还非空但底层文件描述符已经失效。manager 类要做的是在 detach 广播里主动关闭连接private final BroadcastReceiver detachReceiver new BroadcastReceiver() { Override public void onReceive(Context context, Intent intent) { android.hardware.usb.UsbDevice dev intent.getParcelableExtra( android.hardware.usb.UsbManager.EXTRA_DEVICE); if (dev ! null dev.equals(device)) { close(); } } };逻辑说明收到 detach 时把 device 和 connection 同步置空后续业务调用会直接返回-1而不是访问残缺对象。这里用全限定名是为了避免和自定义UsbManager类冲突。4.4 多台同类设备时的选择策略两台同样 VID/PID 的扫描枪同时插入按第一个匹配项返回设备就是碰运气。更可靠的选择顺序是从精确到模糊过滤维度适用场景误用代价VID PID Serial设备固件有唯一序列号部分设备拿不到序列号VID PID 接口类单台设备多台同型号会冲突用户手动选择可交互界面需要额外 UI 流程建议管理器类提供getDeviceList()返回全部候选由调用方按 serial 或端口位置确认而不是类内部擅自选第一台。5. 把 Class_USBManager 做成可自检的黑盒类本身要有自检能力否则出了问题还要在业务代码里加打印效率太低。5.1 加一个 dumpTree()描述符树直接落日志调试时最怕拿到设备却不知道它内部长什么样。给管理器类加一个 dump 方法把整棵描述符树格式化输出public String dumpTree(UsbDevice dev) { StringBuilder sb new StringBuilder(); sb.append(String.format(vid%04x pid%04x%n, dev.getVendorId(), dev.getProductId())); for (int i 0; i dev.getInterfaceCount(); i) { UsbInterface intf dev.getInterface(i); sb.append(String.format( intf%d class%02x subclass%02x%n, i, intf.getInterfaceClass(), intf.getInterfaceSubclass())); for (int j 0; j intf.getEndpointCount(); j) { UsbEndpoint ep intf.getEndpoint(j); sb.append(String.format( ep%d %s max%d%n, ep.getEndpointNumber(), ep.getDirection() UsbConstants.USB_DIR_IN ? IN : OUT, ep.getMaxPacketSize())); } } return sb.toString(); }这段代码直接输出 VID、PID、接口类和每个端点的方向、最大包长。连接失败时把这段日志拉出来基本能判断是描述符解析错误还是端点选错。5.2 超时与缓冲区怎么设不同端点类型对应不同的推荐参数场景超时缓冲区建议控制传输1000-3000ms按协议字段长度Bulk 读1000-3000msmaxPacketSize 的 16 倍Bulk 写300-1000ms按业务帧长最后一个实用技巧在管理器类里维护一个int lastError字段任何公开的 read/write 方法返回负值时都把状态码记录到这个字段并提供getLastError()。下次设备异常时不用猜直接查最近一次失败是发生权限阶段、open 阶段还是 bulk 传输阶段。把 lastError 在每次 connect 成功后清空这样日志里留下的就一定是当前连接周期内的有效错误。本文还有配套的精品资源点击获取
返回列表