基于Android 8.0源码,分析InputManagerService给Physical Keyboard 设置Keyboard Layout(KL)的具体过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| frameworks/native/services/inputflinger/ - InputDispatcher.cpp - InputReader.cpp - InputManager.cpp - EventHub.cpp - InputListener.cpp
frameworks/native/libs/input/ - InputTransport.cpp - Input.cpp - InputDevice.cpp - Keyboard.cpp - KeyCharacterMap.cpp - IInputFlinger.cpp
frameworks/base/services/core/ - java/com/android/server/input/InputManagerService.java - java/com/android/server/input/PersistentDataStore.java - jni/com_android_server_input_InputManagerService.cpp
frameworks/base/packages/InputDevices/ - src/com/android/inputdevices/InputDeviceReceiver.java - res/xml/keyboard_layouts.xml - res/raw/...
|
1. 概述
Gityuan博客有详细分析Input系统,从该博客可以学习到Input模块的工作原理, 以及主要组成:
- Native层的InputReader负责从EventHub取出事件并处理,再交给InputDispatcher
- Native层的InputDispatcher接收来自InputReader的输入事件,并记录WMS的窗口信息,用于派发事件到合适的窗口
- Java层的InputManagerService跟WMS交互,WMS记录所有窗口信息,并同步更新到IMS,为InputDispatcher正确派发事件到ViewRootImpl提供保障
当Physical Keyboard(后面会直接用PK代替) 通过USB与Android设备连接时,首先触发的是硬件驱动,UsbHostManager 识别PK并发出USB_DEVICE_ATTACHED广播, EventHub通过InputReader线程会循环读取消息并调用getEvents()读取输入事件。
调用的流程是:
1
| EventHub::getEvents -> EventHub::scanDevicesLocked -> EventHub::scanDirLocked -> EventHub::openDeviceLocked
|
查看openDeviceLocked方法,可以知道此方法首先打开devicePath,然后new Device,调用LoadKeyMapLocked()来给PK load相应的.kl和.kcm, 所有的.kl文件和.kcm文件都放在/framework/base/data/keyboards/下面。我们可以看到有许多Vendor_XXXX_PRODUCT_XXXX命名的.kl和.kcm file,在loadKeymap时就是根据Device的vendor, product值来查找有没有相对应的Vendor_XXXX_PRODUCT_XXXX文件。多数情况下都不会有与PK对应的.kl和.kcm文件,这时Input系统会load默认的Generic.kl和Generic.kcm给该PK (Generic用的是qwerty layout)。
这也就是为什么有一些法语键盘或者德语键盘链接Android设备之后还是英语的layout的原因。
详细的调用流程可以看源码或者是这篇博文https://blog.csdn.net/kc58236582/.
以上简单介绍了Input系统处理PK连接事件的过程和为其设置.kl和.kcm文件的过程。接下来我将详细介绍一下重新给PK设置Keyboard lauout的过程还有重载新的layout的过程。
2. 为PK重置kl
如上所述,当我们用到非英语键盘时,layout还会加载成qwert的,面对这个问题Input系统已经提供了相应的function供开发者调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| /frameworks/base/services/java/com/android/server/input/InputManagerService.java @Override public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier, InputMethodInfo imeInfo, InputMethodSubtype imeSubtype, String keyboardLayoutDescriptor) { if (!checkCallingPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT, "setKeyboardLayoutForInputDevice()")) { throw new SecurityException("Requires SET_KEYBOARD_LAYOUT permission"); } if (keyboardLayoutDescriptor == null) { throw new IllegalArgumentException("keyboardLayoutDescriptor must not be null"); } if (imeInfo == null) { throw new IllegalArgumentException("imeInfo must not be null"); } InputMethodSubtypeHandle handle = new InputMethodSubtypeHandle(imeInfo, imeSubtype); setKeyboardLayoutForInputDeviceInner(identifier, handle, keyboardLayoutDescriptor); } private void setKeyboardLayoutForInputDeviceInner(InputDeviceIdentifier identifier, InputMethodSubtypeHandle imeHandle, String keyboardLayoutDescriptor) { String key = getLayoutDescriptor(identifier); synchronized (mDataStore) { try { if (mDataStore.setKeyboardLayout(key, imeHandle, keyboardLayoutDescriptor)) { (见2.2.1) if (DEBUG) { Slog.d(TAG, "Set keyboard layout " + keyboardLayoutDescriptor + " for subtype " + imeHandle + " and device " + identifier + " using key " + key); } if (imeHandle.equals(mCurrentImeHandle)) { if (DEBUG) { Slog.d(TAG, "Layout for current subtype changed, switching layout"); } SomeArgs args = SomeArgs.obtain(); args.arg1 = identifier; args.arg2 = imeHandle; mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, args).sendToTarget();(见2.2.3) } mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS); } } finally { mDataStore.saveIfNeeded(); } } }
|
此方法被用在Settings/inputmethod里.
下面我们分析Input系统是如何在底层运作的。
这个package维护的是一个接收“QUERY_KEYBOARD_LAYOUTS”的BroadcastReceiver。
1 2 3 4 5 6 7 8 9
| /src/com/android/inputdevices/InputDeviceReceiver.java <receiver android:name=".InputDeviceReceiver" android:label="@string/keyboard_layouts_label"> <intent-filter> <action android:name="android.hardware.input.action.QUERY_KEYBOARD_LAYOUTS" /> </intent-filter> <meta-data android:name="android.hardware.input.metadata.KEYBOARD_LAYOUTS" android:resource="@xml/keyboard_layouts" /> </receiver>
|
2.1.2 Keyboardlayouts
这里还存放了重要的/res/xml/keyboardlayouts.xml 和 /res/raw/…kcm, /res/raw/下存放了所有语言的.kcm files. xml文件是map所有.kcm文件用的。
1 2 3 4 5 6 7 8 9 10
| <?xml version="1.0" encoding="utf-8"?> <keyboard-layouts xmlns:android="http://schemas.android.com/apk/res/android"> <keyboard-layout android:name="keyboard_layout_english_uk" android:label="@string/keyboard_layout_english_uk_label" android:keyboardLayout="@raw/keyboard_layout_english_uk" /> ... <keyboard-layout android:name="keyboard_layout_latvian" android:label="@string/keyboard_layout_latvian" android:keyboardLayout="@raw/keyboard_layout_latvian_qwerty" /> </keyboard-layouts>
|
此章节将介绍IMS中重要的方法和相应的Class。
2.2.1 PersistentDataStore
IMS通过PersistentDataStore来存储所有的Input devices的信息。在/data/system/input-manager-state.xml中。
通过adb可以查看该文件:
1 2 3 4 5 6 7 8 9 10 11
| /data/system # cat input-manager-state.xml <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <input-manager-state> <input-devices> <input-device descriptor="vendor:16700,product:8467"> <keyboard-layout descriptor="com.android.inputdevices/com.android.inputdevices.InputDeviceReceiver/keyboard_layout_french" input-method-id="com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME" input-method-subtype-id="843948332" current="true" /> </input-device> <input-device descriptor="vendor:1266,product:1027"> <keyboard-layout descriptor="com.android.inputdevices/com.android.inputdevices.InputDeviceReceiver/keyboard_layout_french" input-method-id="com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME" input-method-subtype-id="-921088104" current="true" /> </input-device> </input-devices> </input-manager-state>
|
我们可以看到其中com.android.inputdevices.InputDeviceReceiver/keyboard_layout_french就是对应着2.1.2中的Keyboard-layout name.
IMS.mDataStore就是PersistentDataStore的实例,以上IMS +1391行code就是在set相应的layout值。
2.2.2 visitAllKeyboardLayouts()
这个方法会被native层用到,之后的篇幅会介绍到。此方法主要功能就是遍历2.1.2中的keyboardlayouts.xml维护的所有的layouts。
还有另外一个方法是visitKeyboardLayout()会被Settings.getKeyboardLayout()用到,原理也是一样的,遍历xml找到相应的layout。
具体实现是如下:
1 2 3 4 5 6 7 8 9 10 11
| private void visitAllKeyboardLayouts(KeyboardLayoutVisitor visitor) { final PackageManager pm = mContext.getPackageManager(); Intent intent = new Intent(InputManager.ACTION_QUERY_KEYBOARD_LAYOUTS); for (ResolveInfo resolveInfo : pm.queryBroadcastReceivers(intent, PackageManager.GET_META_DATA | PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE)) { final ActivityInfo activityInfo = resolveInfo.activityInfo; final int priority = resolveInfo.priority; visitKeyboardLayoutsInPackage(pm, activityInfo, null, priority, visitor); } }
|
其中InputManager.ACTION_QUERY_KEYBOARD_LAYOUTS 就是2.1.1中的QUERY_KEYBOARD_LAYOUTS。
2.2.3 handleSwithKeyboardLayout
此方法就是handle IMS +1404行的MSG_SWITCH_KEYBOARD_LAYOUT message。它主要做了两件事:
- mDataStore.switchKeyboardLayout, 将input-manager-state.xml更新为之前setKeyboardLayout(IMS +1391行)的值。
- reloadKeyboardLayouts() -> nativeReloadKeyboardLayouts() 见2.3.1
2.3.1 nativeReloadKeyboardLayouts
1 2 3 4 5 6 7 8
| /frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp static void nativeReloadKeyboardLayouts(JNIEnv* /* env */, jclass /* clazz */, jlong ptr) { NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
im->getInputManager()->getReader()->requestRefreshConfiguration( InputReaderConfiguration::CHANGE_KEYBOARD_LAYOUTS); }
|
主要工作是trigger InputReader::requestRefreshConfiguration()
gityuan的一篇博客中清晰地介绍了InputReader, 对理解下面的代码会很有帮助。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| void InputReader::requestRefreshConfiguration(uint32_t changes) { AutoMutex _l(mLock);
if (changes) { bool needWake = !mConfigurationChangesToRefresh; mConfigurationChangesToRefresh |= changes;
if (needWake) { mEventHub->wake(); } } } ... void InputReader::loopOnce() { int32_t oldGeneration; int32_t timeoutMillis; bool inputDevicesChanged = false; Vector<InputDeviceInfo> inputDevices; { // acquire lock AutoMutex _l(mLock);
oldGeneration = mGeneration; timeoutMillis = -1;
uint32_t changes = mConfigurationChangesToRefresh; if (changes) { mConfigurationChangesToRefresh = 0; timeoutMillis = 0; refreshConfigurationLocked(changes); } else if (mNextTimeout != LLONG_MAX) { nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC); timeoutMillis = toMillisecondTimeoutDelay(now, mNextTimeout); } } // release lock ... } ... void InputReader::refreshConfigurationLocked(uint32_t changes) { mPolicy->getReaderConfiguration(&mConfig); mEventHub->setExcludedDevices(mConfig.excludedDeviceNames);
if (changes) { ... if (changes & InputReaderConfiguration::CHANGE_MUST_REOPEN) { mEventHub->requestReopenDevices(); } else { for (size_t i = 0; i < mDevices.size(); i++) { InputDevice* device = mDevices.valueAt(i); device->configure(now, &mConfig, changes); } } } } ... void InputDevice::configure(nsecs_t when, const InputReaderConfiguration* config, uint32_t changes) { ... if (!isIgnored()) { if (!changes) { // first time only mContext->getEventHub()->getConfiguration(mId, &mConfiguration); }
if (!changes || (changes & InputReaderConfiguration::CHANGE_KEYBOARD_LAYOUTS)) { if (!(mClasses & INPUT_DEVICE_CLASS_VIRTUAL)) { sp<KeyCharacterMap> keyboardLayout = mContext->getPolicy()->getKeyboardLayoutOverlay(mIdentifier); (见2.4) if (mContext->getEventHub()->setKeyboardLayoutOverlay(mId, keyboardLayout)) { bumpGeneration();(见2.5) } } } ... }
|
这部分的代码主要工作是
- 把InputReaderConfiguration::CHANGE_KEYBOARD_LAYOUTS换位给
mConfigurationChangesToRefresh
- InputReader线程不停的loop,当mConfigurationChangesToRefresh不为0时,调用refreshConfigurationLocked方法
- 当changes值不为CHANGE_MUST_REOPEN时,将循环所有的InputDevice,并调用configure方法
- 当changes是CHANGE_KEYBOARD_LAYOUTS时 会调用两个重要的方法,将会再下两个小节详细介绍
2.4 getPolicy()
如Gityuan博客中提到的,InputReader的成员变量mPolicy都是指NativeInputManager对象。这里getPolicy()得到的就是NativeInputManager对象。接下来我们看一下getKeyboardLayoutOverlay方法的具体工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| /frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp public: NativeInputManager(jobject contextObj, jobject serviceObj, const sp<Looper>& looper); ... /* --- InputReaderPolicyInterface implementation --- */ ... virtual sp<KeyCharacterMap> getKeyboardLayoutOverlay(const InputDeviceIdentifier& identifier); ... int register_android_server_InputManager(JNIEnv* env) { int res = jniRegisterNativeMethods(env, "com/android/server/input/InputManagerService", gInputManagerMethods, NELEM(gInputManagerMethods)); (void) res; // Faked use when LOG_NDEBUG. LOG_FATAL_IF(res < 0, "Unable to register native methods.");
// Callbacks
jclass clazz; FIND_CLASS(clazz, "com/android/server/input/InputManagerService"); ... GET_METHOD_ID(gServiceClassInfo.getKeyboardLayoutOverlay, clazz, "getKeyboardLayoutOverlay", "(Landroid/hardware/input/InputDeviceIdentifier;)[Ljava/lang/String;");
|
主要工作就是通过JNI调用JAVA层的InputManagerService.getKeyboardLayoutOverlay()方法。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| // Native callback. private String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) { if (!mSystemReady) { return null; }
String keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier); if (keyboardLayoutDescriptor == null) { return null; }
final String[] result = new String[2]; visitKeyboardLayout(keyboardLayoutDescriptor, new KeyboardLayoutVisitor() { @Override public void visitKeyboardLayout(Resources resources, int keyboardLayoutResId, KeyboardLayout layout) { try { result[0] = layout.getDescriptor(); result[1] = Streams.readFully(new InputStreamReader( resources.openRawResource(keyboardLayoutResId))); } catch (IOException ex) { } catch (NotFoundException ex) { } } }); ... }
|
可以看到它的主要工作是
- 从InputDeviceIdentifier中拿到keyboardLayoutDescriptor
- 调用visitKeyboardLayout()去遍历kayboardlayouts.xml找到对应的layout resource文件并返回。见2.2.2。
2.5 EventHub::setKeyboardLayoutOverlay()
从2.4节得到的keyboardlayout result作为参数,通过EventHub设置给相应的Device。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| bool EventHub::setKeyboardLayoutOverlay(int32_t deviceId, const sp<KeyCharacterMap>& map) { AutoMutex _l(mLock); Device* device = getDeviceLocked(deviceId); if (device) { if (map != device->overlayKeyMap) { device->overlayKeyMap = map; device->combinedKeyMap = KeyCharacterMap::combine( device->keyMap.keyCharacterMap, map); return true; } } return false; }
|
最终实现为PK设置非英语的keyboard layout。
3. 总结
Native callback 感觉是最有意思的地方,以后可以用一下,JNI层还是需要多加研究的。对于Input系统,算是通过这次机会了解到这么多细节的东西,但是还有InputDispatch的部分还需要日后仔细学习。