Android USB 输入设备

一、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鼠标设备,观察到如下现象:

  1. 插入 USB 键盘,提示 USB 键盘已插入,拔掉该设备,提示 USB 键盘已删除。
  2. 插入 USB 鼠标,提示 USB 鼠标已插入,拔掉该设备,提示 USB 鼠标已删除。

      于是,形成了早起的需求,并根据对API的理解,写下了如下代码:

          各个函数/字段大意:

  • init:客户端实例化 InputManager,通过向其注册 InputDeviceListener 来动态监听设备的插入、拔出、设备属性变化等信息:
  • release:客户端将 InputDeviceListener 从 InputManager 中解除注册;
  • mInputDeviceListener:实现了 InputManager.InputDeviceListener 接口的对象,用于注册进 InputManager,来观察输入设备的热插拔和性质变化。
  • mInputDeviceListener#onInputDeviceAdded:输入设备键入Android Host完成供电并且可以交互的回调,简单理解为设备插入。
  • mInputDeviceListener#onInputDeviceRemoved:输入设备从Android Host交出交互传输的回调,简单理解为设备拔除。
  • mInputDeviceListener#onInputDeviceChanged:输入设备在工作过程中,某些性质发生变化的回调。
private InputManager mInputManager;
private Context mContext;
private Map<Integer, InputDevice> 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; 
}

根据如上信息,算是大致完成了需求,但在客户测试过程中,报出了非常多的问题,这里摘主要的:

  1. 插入 USB 耳机,提示 USB 键盘已连接;拔掉 USB 耳机,提示 USB 键盘已删除;
  2. 插入 USB 鼠标,提示 USB 键盘已连接;拔掉 USB 鼠标,提示 USB 键盘已删除;

经过不断和客户沟通,他们仍无法提供具体的需求 SPEC、测试设备清单、甚至是软件行为逻辑给到我们。由于和客户是异地协作,我们制作软件,他们测试,所以借设备是不可能(之前出差在客户现场支持,借设备也很难)。

但确认问题这个过程,我们依然要做,在条件有限的情况下,我们也只能做这么几件事:

        其一,咨询其测试设备的品牌、型号,在公司内寻找,必要时可以申请购买;

        其二,了解输入设备类型知识,尽量多借一些不同品类、同品类不同型号的设备,对客户样机进行把玩,并且做好记录。

        其三,客户样机具备 adb Debug 能力,从里面打捞出客户的应用软件,进行反向解析,看看能不能找到其他额外的信息。

通过第一个动作,迅速将两个问题的相关信息和范围进行了锁定:

  • 问题1:USB Dongle 耳机品牌为:UGREEN绿联,型号为:US205,插入该设备,样机设备无任何提示,拔出也是一样的。
  • 问题2:USB 鼠标品牌为:罗技,信号为:G304,插入该设备,应该提示为:USB 鼠标已连接,拔出时提示:USB 鼠标已删除。

通过第二个动作,将整理成的记录变为了需求点,从而将模糊的需求,明确到了这种程度:

  1. 插入 USB 键盘,提示 USB 键盘已插入,拔掉该设备,提示 USB 键盘已删除。
  2. 插入 USB 鼠标,提示 USB 鼠标已插入,拔掉该设备,提示 USB 鼠标已删除。
  3. 插入 USB Dongle 耳机,参考样机逻辑(有的情况不提示,有的情况提示键盘...),存在兼容性问题。
  4. 隐性需求1:开机时,如果已经插入了 USB 输入设备,参考样机行为;STR 关开机之前如果已经插入了 USB 输入设备,参考样机行为;
  5. 隐性需求2:客户无法提供 Audio 类产品的行为逻辑;也无法提供需要兼容设备的类型和设备列表;我们通过不同设备进行模拟,大致确认了客户只需兼容肉眼可识别的鼠标、键盘。
  6. 隐性需求3:同类型同型号的设备兼容考虑,主要是同时插拔的场景。客户样机只有一个 USB 口可用,通过 USB 拓展坞观察。

通过第三个动作,成功的找到了如下有用的信息:

  1. 成功的找到了客户软件里面的 UI 资源包、APK主逻辑包
  2. 成功的对主逻辑包进行了反向编译,并且发现客户有对一些特殊设备做过滤处理,大致分了三大类:Audio 类设备、部分USB Dongle 设备、P客户特定的遥控器设备。
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<String> 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:

方式编号方式介绍特点限制
方式1USB Tree Viewer工具开源,基于 Windows 的输入设备API,列举设备的所有描述信息和设备性质,好用只有 windows 电脑可以使用,Mac 目前无法兼容,虚拟机兼容差
方式2InputManagerListener根据回调可以得知插入的设备的关键信息,含:设备名称、ID、描述、设备性质等缺失USB的节点信息,设备信息基本够用
方式3USB 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 的学习之路上共同进步,知识越累越多,问题越来越少。

Android系统支持USB键盘输入和读卡器的处理。在连接USB键盘时,Android系统会自动识别并允许用户使用键盘进行输入操作。用户可以通过插入USB键盘,然后在系统设置中进行一些简单配置,如选择键盘布局、调整设置等。 读卡器方面,Android系统也提供了相应的API接口,允许开发者进行读卡器的数据读取和处理。通过连接读卡器至Android设备USB接口,应用程序可以利用系统提供的API与读卡器进行通信,并对读取到的数据进行解析和处理。这样,用户可以用读卡器来读取各种类型的卡片,如信用卡、身份证、银行卡等,以完成相关业务需求。 当连接USB键盘和读卡器时,Android系统会检测到设备的插入,并根据插入设备的类型和功能进行相应的处理。系统将自动为插入设备加载相应的驱动程序,并在系统中提供相应的接口供应用程序进行调用。这样,应用程序可以通过监听、识别和解析USB键盘和读卡器的数据,以实现特定的业务逻辑,如数据输入、读卡等功能。 总之,Android系统可以很好地处理USB键盘输入和读卡器的功能。用户可以通过简单的设置配置USB键盘,方便地进行键盘输入操作。同时,通过使用系统提供的API接口,应用程序可以与连接的读卡器进行通信,并对读取到的卡片数据进行处理。这为用户提供了更多的可能性,可以方便地进行各种操作和业务需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值