Analyze the Process of Setting KL for Physical Keyboard

基于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系统是如何在底层运作的。

2.1 InputDevices

2.1.1 InputDeviceReceiver

这个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>

2.2 InputManagerService

此章节将介绍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 Native InputManager

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()

2.3.2 InputReader.cpp

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的部分还需要日后仔细学习。