HID:Human Interface Device。如鼠标、键盘、游戏手柄等;
本文解决方法为系统源码级,非APP解决方案,主要分析流程及原因。
如下正文开始:
关于使用UsbManager获取HID设备的方法,网上有很多文章说明,基本使用如下:
UsbManager manager = (UsbManager) m_context.getSystemService(Context.USB_SERVICE);
HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
拿到deviceList之后,再遍历设备列表获取设备信息展示。但是上述方案基本都存在一个问题,就是对于大部分HID设备,比如鼠标,使用上述方法无法获取其相关信息,连接成功后,APP拿到的deviceList为空。
网上的旧文一般有两种方案:
1. 在/system/etc/permissions路径下增加所谓的android.hardware.usb.host.xml权限,本人使用Android R版本亲测无效。
2. 使用InputManager替代UsbManager,可以获取鼠标设备信息,但是只能读取,无法双向交互,不能满足需求。
为解决上述问题,我们再回到最开始的关键点,为何使用UsbManager无法获取鼠标设备信息。
第一步,从USBManager的getDeviceList入手来看下其实现:
371 /**
372 * Returns a HashMap containing all USB devices currently attached.
373 * USB device name is the key for the returned HashMap.
374 * The result will be empty if no devices are attached, or if
375 * USB host mode is inactive or unsupported.
376 *
377 * @return HashMap containing all connected USB devices.
378 */
379 @RequiresFeature(PackageManager.FEATURE_USB_HOST)
380 public HashMap<String,UsbDevice> getDeviceList() {
381 HashMap<String,UsbDevice> result = new HashMap<String,UsbDevice>();
382 if (mService == null) {
383 return result;
384 }
385 Bundle bundle = new Bundle();
386 try {
387 mService.getDeviceList(bundle);
388 for (String name : bundle.keySet()) {
389 result.put(name, (UsbDevice)bundle.get(name));
390 }
391 return result;
392 } catch (RemoteException e) {
393 throw e.rethrowFromSystemServer();
394 }
395 }
可以看出,其数据来源于mService.getDeviceList(bundle)方法,mService为IUsbManager类型,也就是一个aidl接口。要知道其真正做了什么,还需要看调用的具体实现类。
第二步,顺着IUsbManager这个线索继续往下追查,很容易就找到了UsbService这里。具体路径在:/frameworks/base/services/usb/java/com/android/server/usb/UsbService.java
我们继续观察其关键方法getDeviceList,看具体实现
225 /* Returns a list of all currently attached USB devices (host mdoe) */
226 @Override
227 public void getDeviceList(Bundle devices) {
228 if (mHostManager != null) {
229 mHostManager.getDeviceList(devices);
230 }
231 }
于是,进一步追踪mHostManager这个对象
private UsbHostManager mHostManager;
第三步,我们再来看UsbHostManager这个类中的getDeviceList方法。
436 /* Returns a list of all currently attached USB devices */
437 public void getDeviceList(Bundle devices) {
438 synchronized (mLock) {
439 for (String name : mDevices.keySet()) {
440 devices.putParcelable(name, mDevices.get(name));
441 }
442 }
443 }
可见其数据来自于mDevices这个对象,这是一个HashMap存储类型,存储了所有当前连接的USB设备信息。
// contains all connected USB devices
70 private final HashMap<String, UsbDevice> mDevices = new HashMap<>();
跟踪这个对象的赋值点,我们最终来到了usbDeviceAdded这个关键方法中。
338 /* Called from JNI in monitorUsbHostBus() to report new USB devices
339 Returns true if successful, i.e. the USB Audio device descriptors are
340 correctly parsed and the unique device is added to the audio device list.
341 */
342 @SuppressWarnings("unused")
343 private boolean usbDeviceAdded(String deviceAddress, int deviceClass, int deviceSubclass,
344 byte[] descriptors) {
345 if (DEBUG) {
346 Slog.d(TAG, "usbDeviceAdded(" + deviceAddress + ") - start");
347 }
348
349 if (isBlackListed(deviceAddress)) {
350 if (DEBUG) {
351 Slog.d(TAG, "device address is black listed");
352 }
353 return false;
354 }
355 UsbDescriptorParser parser = new UsbDescriptorParser(deviceAddress, descriptors);
356 logUsbDevice(parser);
357
358 if (isBlackListed(deviceClass, deviceSubclass)) {
359 if (DEBUG) {
360 Slog.d(TAG, "device class is black listed");
361 }
362 return false;
363 }
364
365 synchronized (mLock) {
366 if (mDevices.get(deviceAddress) != null) {
367 Slog.w(TAG, "device already on mDevices list: " + deviceAddress);
368 //TODO If this is the same peripheral as is being connected, replace
369 // it with the new connection.
370 return false;
371 }
372
373 UsbDevice newDevice = parser.toAndroidUsbDevice();
374 if (newDevice == null) {
375 Slog.e(TAG, "Couldn't create UsbDevice object.");
376 // Tracking
377 addConnectionRecord(deviceAddress, ConnectionRecord.CONNECT_BADDEVICE,
378 parser.getRawDescriptors());
379 } else {
380 mDevices.put(deviceAddress, newDevice);
381 Slog.d(TAG, "Added device " + newDevice);
382
383 // It is fine to call this only for the current user as all broadcasts are
384 // sent to all profiles of the user and the dialogs should only show once.
385 ComponentName usbDeviceConnectionHandler = getUsbDeviceConnectionHandler();
386 if (usbDeviceConnectionHandler == null) {
387 getCurrentUserSettings().deviceAttached(newDevice);
388 } else {
389 getCurrentUserSettings().deviceAttachedForFixedHandler(newDevice,
390 usbDeviceConnectionHandler);
391 }
392
393 mUsbAlsaManager.usbDeviceAdded(deviceAddress, newDevice, parser);
394
395 // Tracking
396 addConnectionRecord(deviceAddress, ConnectionRecord.CONNECT,
397 parser.getRawDescriptors());
398 }
399 }
400
401 if (DEBUG) {
402 Slog.d(TAG, "beginUsbDeviceAdded(" + deviceAddress + ") end");
403 }
404
405 return true;
406 }
这个方法是本文的重点所在,其中有几个关键的注释和判断条件。
1.先看这个注释“Called from JNI in monitorUsbHostBus() to report new USB devices Returns true if successful”,该方法为usb设备插入时触发,且直接通过JNI方法调用,并不暴露给Android上层应用。
2.在349/358行分别有两个对应的黑名单判断方法对部分设备做了拦截:isBlackListed
所以这里很大可能是由于鼠标设备在接入时,被此处拦截,所以Android上层获取不到。
带着这个猜想我们来做验证。
首先:打开DEBUG开关,这里默认值为false,全编版本。刷机测试,接入鼠标设备,查看log信息,确实走到了上面第二个isBlackListed方法的判断里。我们的猜测是正确的。
358 if (isBlackListed(deviceClass, deviceSubclass)) {
359 if (DEBUG) {
360 Slog.d(TAG, "device class is black listed");
361 }
362 return false;
363 }
到这里,其实我们已经大致搞清楚了为什么APP无法通过USBManager类的公开方法getDeviceList获取鼠标设备信息了,原因就是在这块被拦截掉了。
接下来我们顺着这个方法继续往下看,是否有修改方案。
先看这个黑名单判断方法。这里有两个参数clazz和subClass,通过对其类型匹配进行拦截。
280 /* returns true if the USB device should not be accessible by applications */
281 private boolean isBlackListed(int clazz, int subClass) {
282 // blacklist hubs
283 if (clazz == UsbConstants.USB_CLASS_HUB) return true;
284
285 // blacklist HID boot devices (mouse and keyboard)
286 return clazz == UsbConstants.USB_CLASS_HID
287 && subClass == UsbConstants.USB_INTERFACE_SUBCLASS_BOOT;
288
289 }
其中还有句关键注释“blacklist HID boot devices (mouse and keyboard)”,这里意思已经很明显了,不过值得注意的是,本人拿了一个普通USB口键盘进行测试,发现还是可以识别的。
也就是说鼠标插入时,是满足这个判断条件的,所以这里return true,直接被拦截。那么想要获取鼠标设备,只要保证其不在这里被拦截是否就可以?
继续测试,将该方法后面的return条件直接修改为return false,也就是不做拦截。全编译代码,刷机测试,链接鼠标设备后,确实可以通过UsbManager直接获取设备信息。因此,若想修改这个设定,还是要在源码这里进行判断,给自己开特殊通道,问题得以解决。