一、Android 官方定义:
用于描述输入设备的信息、性质、功能。
每个输入设备可以支持多类输入,例如,多功能键盘可以将标准键盘的功能与触控板鼠标或其他定点设备组合在一起。
一些输入设备可以呈现出多个可区分的输入源,比如你的USB Dongle鼠标,它可能具备键盘、鼠标、游戏摇杆的性质。
二、Android 应用层关联的API:
// 输入设备管理类:
android.hardware.input.InputManager
// 输入设备的监听器:用于监听 USB 设备的热插拔、信号变化
android.hardware.input.InputManager$InputDeviceListener
// 输入设备的描述类:
android.view.InputDevice
三、Android USB 输入设备的软件架构:
四、对于客户端开发而言,只要关注 IInputManager.aidl 的接口定义,通过 InputManager/InputDevice 的API调用即可完成基本的功能开发,可以在线访问:IInputManager.aidl Android 9.0 接口线上访问地址。
五、客户需求:参考样机 MTK9632(7007)样机,对于插入的设备进行检测,并弹出对应提示(无法给出具体的SPEC、行为逻辑)。
根据上面需求,红庆对样机做了基本的实验,运用了USB键盘、USB鼠标设备,观察到如下现象:
于是,形成了早起的需求,并根据对API的理解,写下了如下代码:
各个函数/字段大意:
private InputManager mInputManager;
private Context mContext;
private Map mDeviceMap = new HashMap<>();
private InputManager.InputDeviceListener mInputDeviceListener = new InputManager.InputDeviceListener() {
@Override
public void onInputDeviceAdded(int deviceId) {
// 当设备插入时,回调此函数
InputDevice device = mInputManager.getInputDevice(deviceId);
if (device == null) {
return;
}
if (isKeyBoard(device)) {
mDeviceMap.put(deviceId, device);
showToast("USB键盘已连接");
} else if (isMouse(device)) {
mDeviceMap.put(deviceId, device);
showToast("USB鼠标已连接");
}
}
@Override
public void onInputDeviceRemoved(int deviceId) {
// 当设备移除时,回调此函数(设备移除时,无法获取到USB设备的信息,所以上面的MAP就起到了作用)
InputDevice device = mDeviceMap.remove(deviceId);
if (device == null) {
return;
}
if (isKeyBoard(device)) {
mDeviceMap.put(deviceId, device);
showToast("USB键盘已删除");
} else if (isMouse(device)) {
mDeviceMap.put(deviceId, device);
showToast("USB鼠标已删除");
}
}
@Override
public void onInputDeviceChanged(int deviceId) {
// 当设备性质发生动态变化时,回调此函数
}
}
void init(Context context) {
this.mContext = context;
// 1. 实例化输入管理器,基于 Binder 的调用,通过 ServiceManager 向 SystemServer 进程发起服务别名为“input”的服务查询,略
mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
// 2. 向输入管理器中注册设备监听器,Native 层的 InputReader.cpp 识别到设备变化,向 SystemServer 进程发送刷列表的消息,notifyInputDevicesChanged,收到消息后,向Binder客户端回传设备信息。
mInputManager.registerInputDeviceListener(mInputDeviceListener);
}
void release(Context context) {
// 移除设备管理器中的设备监听器
mInputManager.unregisterInputDeviceListener(mInputDeviceListener);
}
void showToast(String content) {
Toast.makeText(mContext, content, Toast.LENGTH_SHORT).show();
}
boolean isKeyBoard(InputDevice device) {
int sourceMask = device.getSources();
return sourceMask & InputDevice.SOURCE_KEYBOARD == InputDevice.SOURCE_KEYBOARD;
}
boolean isMouse(InputDevice device) {
int sourceMask = device.getSources();
return sourceMask & InputDevice.SOURCE_MOUSE == InputDevice.SOURCE_MOUSE;
}
根据如上信息,算是大致完成了需求,但在客户测试过程中,报出了非常多的问题,这里摘主要的:
经过不断和客户沟通,他们仍无法提供具体的需求 SPEC、测试设备清单、甚至是软件行为逻辑给到我们。由于和客户是异地协作,我们制作软件,他们测试,所以借设备是不可能(之前出差在客户现场支持,借设备也很难)。
但确认问题这个过程,我们依然要做,在条件有限的情况下,我们也只能做这么几件事:
其一,咨询其测试设备的品牌、型号,在公司内寻找,必要时可以申请购买;
其二,了解输入设备类型知识,尽量多借一些不同品类、同品类不同型号的设备,对客户样机进行把玩,并且做好记录。
其三,客户样机具备 adb Debug 能力,从里面打捞出客户的应用软件,进行反向解析,看看能不能找到其他额外的信息。
通过第一个动作,迅速将两个问题的相关信息和范围进行了锁定:
通过第二个动作,将整理成的记录变为了需求点,从而将模糊的需求,明确到了这种程度:
通过第三个动作,成功的找到了如下有用的信息:
static boolean isAudio(InputDevice device) {
String name = device.getName();
return !TextUtils.isEmpty(name) && (name.contains("Audio") || name.contains("audio") || name.toLowerCase().contains("audio") || name.toUpperCase().contains("AUDIO"));
}
private final static List mInputDeviceNameBackList = Arrays.asList(
// Philips Settings 已经过滤的不提示的设备列表(USB Dongle 设备列表)
"Wireless Gamepad F710",
"Logitech Cordless RumblePad 2",
"Bluetooth Mouse M557",
"Bluetooth Mouse M336/M337/M535",
// Philips Settings 已经过滤的不提示的 Philips 品牌遥控器的设备列表(Philips 品牌遥控器列表)
"PHLRC",
"PHLCB",
"PHL45C2",
"PHL44CB",
"A15BC",
"P45C2B",
"PHL44C",
"Huitong BLE Remote",
"RCSP"
);
六、综合上述需求,业务逻辑方面的问题算是比较清晰了,代码也比较好完善,此处略过。
这里的关键是分析并处理客户报出的两个类型不正确的问题,即 为何 USB Dongle 的耳机、USB Dongle 的鼠标 为何会提示 USB 键盘已连接、已删除?
这个问题,根据知识面,可以通过这么几个方式来 Debug:
方式编号 | 方式介绍 | 特点 | 限制 |
---|---|---|---|
方式1 | USB Tree Viewer | 工具开源,基于 Windows 的输入设备API,列举设备的所有描述信息和设备性质,好用 | 只有 windows 电脑可以使用,Mac 目前无法兼容,虚拟机兼容差 |
方式2 | InputManagerListener | 根据回调可以得知插入的设备的关键信息,含:设备名称、ID、描述、设备性质等 | 缺失USB的节点信息,设备信息基本够用 |
方式3 | USB Host Mode | 可以观测 USB 主机口热插拔的信息,映射为系统设备的节点,类似 fd 的信息 | 节点信息完整,设备信息模糊,无法形成较为准确的判断。 |
由上述对分析方式的说明,可以得知:方式1 和方式2 对分析、解决问题有较为直接的帮助。接下来就是技术方案的确定了,如果是 windows 电脑,我建议方式1、方式2都可以尝试。如果是其他型号电脑,我建议按方式2 分析。
下面我按方式2,给出调试代码与设备兼容性日志:
// Debug 日志TAG,为了方便过滤、分析,暂用姓名(正式软件中,需要按编码要求修正)
private static final String TAG = "zhangfan";
private InputManager.InputDeviceListener mInputDeviceListener = new InputManager.InputDeviceListener() {
@Override
public void onInputDeviceAdded(int deviceId) {
// 当设备插入时,回调此函数
InputDevice device = mInputManager.getInputDevice(deviceId);
Log.d(TAG, "onInputDeviceAdded: inputDevice = " + device);
if (device == null) {
return;
}
showInputDeviceMessage(device);
if (isKeyBoard(device)) {
mDeviceMap.put(deviceId, device);
showToast("USB键盘已连接");
} else if (isMouse(device)) {
mDeviceMap.put(deviceId, device);
showToast("USB鼠标已连接");
}
}
@Override
public void onInputDeviceRemoved(int deviceId) {
// 当设备移除时,回调此函数(设备移除时,无法获取到USB设备的信息,所以上面的MAP就起到了作用)
InputDevice device = mDeviceMap.remove(deviceId);
Log.d(TAG, "onInputDeviceRemoved: inputDevice = " + device);
if (device == null) {
return;
}
showInputDeviceMessage(device);
if (isKeyBoard(device)) {
mDeviceMap.put(deviceId, device);
showToast("USB键盘已删除");
} else if (isMouse(device)) {
mDeviceMap.put(deviceId, device);
showToast("USB鼠标已删除");
}
}
@Override
public void onInputDeviceChanged(int deviceId) {
// 当设备性质发生动态变化时,回调此函数
}
}
void init(Context context) {
this.mContext = context;
// 1. 实例化输入管理器,基于 Binder 的调用,通过 ServiceManager 向 SystemServer 进程发起服务别名为“input”的服务查询,略
mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
// 2. 向输入管理器中注册设备监听器,Native 层的 InputReader.cpp 识别到设备变化,向 SystemServer 进程发送刷列表的消息,notifyInputDevicesChanged,收到消息后,向Binder客户端回传设备信息。
mInputManager.registerInputDeviceListener(mInputDeviceListener);
printInputDeviceConstants();
}
private void showInputDeviceMessage(@NonNull InputDevice device) {
Log.d(TAG, "\n\n");
String name = device.getName();
int keyboardType = device.getKeyboardType();
int sourceMask = device.getSources();
int resultMask1 = sourceMask | InputDevice.SOURCE_MOUSE;
int resultMask2 = sourceMask | InputDevice.SOURCE_CLASS_POINTER;
int resultMask3 = sourceMask | InputDevice.SOURCE_KEYBOARD;
int resultMask4 = sourceMask | InputDevice.SOURCE_CLASS_BUTTON;
Log.d(TAG, "showInputDeviceMessage: name = " + name);
Log.d(TAG, "showInputDeviceMessage: keyboardType = " + keyboardType);
Log.d(TAG, "showInputDeviceMessage: sourceMask = " + sourceMask);
Log.d(TAG, "showInputDeviceMessage: resultMask1 = " + resultMask1);
Log.d(TAG, "showInputDeviceMessage: resultMask2 = " + resultMask2);
Log.d(TAG, "showInputDeviceMessage: resultMask3 = " + resultMask3);
Log.d(TAG, "showInputDeviceMessage: resultMask4 = " + resultMask4);
}
private void printInputDeviceConstants() {
Log.d(TAG, "\n\n");
Log.d(TAG, "printInputDeviceConstants: SOURCE_CLASS_MASK = " + InputDevice.SOURCE_CLASS_MASK);
Log.d(TAG, "printInputDeviceConstants: SOURCE_CLASS_NONE = " + InputDevice.SOURCE_CLASS_NONE);
Log.d(TAG, "printInputDeviceConstants: SOURCE_CLASS_BUTTON = " + InputDevice.SOURCE_CLASS_BUTTON);
Log.d(TAG, "printInputDeviceConstants: SOURCE_CLASS_POINTER = " + InputDevice.SOURCE_CLASS_POINTER);
Log.d(TAG, "printInputDeviceConstants: SOURCE_CLASS_TRACKBALL = " + InputDevice.SOURCE_CLASS_TRACKBALL);
Log.d(TAG, "printInputDeviceConstants: SOURCE_CLASS_POSITION = " + InputDevice.SOURCE_CLASS_POSITION);
Log.d(TAG, "printInputDeviceConstants: SOURCE_CLASS_JOYSTICK = " + InputDevice.SOURCE_CLASS_JOYSTICK);
Log.d(TAG, "printInputDeviceConstants: SOURCE_KEYBOARD = " + InputDevice.SOURCE_KEYBOARD);
Log.d(TAG, "printInputDeviceConstants: SOURCE_DPAD = " + InputDevice.SOURCE_DPAD);
Log.d(TAG, "printInputDeviceConstants: SOURCE_GAMEPAD = " + InputDevice.SOURCE_GAMEPAD);
Log.d(TAG, "printInputDeviceConstants: SOURCE_TOUCHSCREEN = " + InputDevice.SOURCE_TOUCHSCREEN);
Log.d(TAG, "printInputDeviceConstants: SOURCE_MOUSE = " + InputDevice.SOURCE_MOUSE);
Log.d(TAG, "printInputDeviceConstants: SOURCE_STYLUS = " + InputDevice.SOURCE_STYLUS);
Log.d(TAG, "printInputDeviceConstants: SOURCE_BLUETOOTH_STYLUS = " + InputDevice.SOURCE_BLUETOOTH_STYLUS);
Log.d(TAG, "printInputDeviceConstants: SOURCE_TRACKBALL = " + InputDevice.SOURCE_TRACKBALL);
Log.d(TAG, "printInputDeviceConstants: SOURCE_MOUSE_RELATIVE = " + InputDevice.SOURCE_MOUSE_RELATIVE);
Log.d(TAG, "printInputDeviceConstants: SOURCE_ROTARY_ENCODER = " + InputDevice.SOURCE_ROTARY_ENCODER);
Log.d(TAG, "printInputDeviceConstants: SOURCE_JOYSTICK = " + InputDevice.SOURCE_JOYSTICK);
Log.d(TAG, "printInputDeviceConstants: SOURCE_HDMI = " + InputDevice.SOURCE_HDMI);
Log.d(TAG, "printInputDeviceConstants: SOURCE_ANY = " + InputDevice.SOURCE_ANY);
}
添加了丰富的日志后,我们插上 USB Audio 类型的外设后,惊人的看到了如下打印,它虽然是 Audio 设备,但系统返回的却是 keyboard 性质的设备,被咱们的代码判断为键盘了:
于是,插入了其他类型的 Audio 设备,也有类似的情况出现。与客户对比机对比,客户无相关提示,咱们提示了 “USB 键盘已连接、已删除”,结合客户测试人员说插入 USB 耳机应该无提示,那么此处的逻辑就比较好处理了。
按照同样的思路,分析了问题2:USB 鼠标被识别为 USB 键盘,提示了 “USB 键盘已连接、已删除”。
借到了同事的 USB 罗技M590鼠标、小米 2.4G USB Dongle 鼠标、有线鼠标分别模拟了热插拔情况,于是,我们发现了另一幕:
上面的是逻辑 M590 型号鼠标的信息,当设备插入后,系统发起了两次 DeviceAdd 的信息,第一次的设备信息包含(keyboard、dpad)两种性质,第二次的设备信息包含(keyboard、dpad、mouse、joystick)四种性质。
从这里,我们可能会有一个问题:为何我插入了 USB Dongle 鼠标,会收到两次系统的消息回调呢?
其实这个和设备的工作原理有关,USB的热插拔特性和线序可以回答为什么有第一次。
另外,USB Dongle鼠标的工作原理是需要和 Dongle 进行链接,当 USB Dongle 插入电视,被电视供电后,鼠标发射信号与 Dongle 进行链接,触发了系统的第二次消息通知,相当于是把鼠标控制的特性添加到系统识别的输入设备列表中。
我们的软件判断为键盘,显示为 “USB 键盘已连接、已删除”,而客户的软件判断为鼠标,显示为 “USB 鼠标已连接、已删除”。
从这里,大概可以判断出,客户的软件显示了是按照最后一次消息通知来判断设备属性的,而这种较为复杂的多功能外设,优先判断了是否是鼠标,如果有鼠标性质,则不会在判断其他性质,否则再判断是否为键盘。
在我们插入了 小米 2.4G USB Dongle 鼠标的那一刻,刚刚的想法就被证实了。
插入此设备后,一共收到 3 次系统通知,第一次的设备信息包含(keyboard、dpad)两种性质,第二次的设备信息包含(keyboard、mouse)两种性质,第三次的设备信息包含(keyboard、dpad、joystick)三种性质。
按照我们之前的想法,这次客户样机显示的应该是“USB 键盘已连接、已删除”,经过验证,果然如此。
再通过一起其他多功能的鼠标验证,基本说明了我们的判断是正确的,与客户确认,他们也理解了相关做法(对接的人不懂技术)。
经过如上分析,问题的修复措施就相对清晰了起来。
简而言之,只需要在收到add设备的消息时,忽略短时间内的前有提示,按最后一次设备信息中的鼠标、键盘来确认性质即可。
至此,问题的分析基本结束,接下来就是构建业务逻辑代码和细节优化的部分,相对简单就省略带过了。
七、工具使用:
推荐也学习一下 USB Tree Viewer 工具,真的好用,在这个上面可以看到复杂外设的多种性质,而且描述的更为细致、全面,从这个工具展示的效果来看,Windows 比 Android 的接口要丰富,且通用性更强。
也推荐了解下 USB 下的 HM、AM 模式,对需要嵌入式设备通信有开发需要的小伙伴,比较有用。
另外,USB 的调试,也有很多其他的方法,涉及到的范围不尽相同,可以看接下来的一篇关于 Linux USB 设备调试的实用命令。
最后,希望大家在 USB 的学习之路上共同进步,知识越累越多,问题越来越少。