From a7779bf6623c1bc48a33e3e7d926884d6675f94a Mon Sep 17 00:00:00 2001 From: Vaibhav Devmurari Date: Mon, 10 Oct 2022 16:50:16 +0000 Subject: [PATCH] Add internal APIs to support modifier key remapping Add support for remapping 4 modifier keys: Ctrl, Meta, Alt and Shift to one another. Use new addKeyRemapping native API to add key remappings for supported modifier keys. Explicitly define supported modifier keys to prevent misuse to remap other Android key codes. Test: atest KeyboardLayoutChangeTest Bug: 252812993 Change-Id: I6d03b7dba37b9ec9cef3dca98f7081ef0a378447 --- core/api/test-current.txt | 4 + .../android/hardware/input/IInputManager.aidl | 16 ++ .../android/hardware/input/InputManager.java | 80 +++++++++ core/res/AndroidManifest.xml | 8 + packages/Shell/AndroidManifest.xml | 3 + .../server/input/InputManagerService.java | 26 +++ .../com/android/server/input/KeyRemapper.java | 161 ++++++++++++++++++ .../input/NativeInputManagerService.java | 5 + .../server/input/PersistentDataStore.java | 61 ++++++- ...droid_server_input_InputManagerService.cpp | 7 + .../android/server/input/KeyRemapperTests.kt | 150 ++++++++++++++++ 11 files changed, 516 insertions(+), 5 deletions(-) create mode 100644 services/core/java/com/android/server/input/KeyRemapper.java create mode 100644 services/tests/servicestests/src/com/android/server/input/KeyRemapperTests.kt diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 25c4652e9112a..ddf75782fca48 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -38,6 +38,7 @@ package android { field public static final String READ_CELL_BROADCASTS = "android.permission.READ_CELL_BROADCASTS"; field public static final String READ_PRIVILEGED_PHONE_STATE = "android.permission.READ_PRIVILEGED_PHONE_STATE"; field public static final String RECORD_BACKGROUND_AUDIO = "android.permission.RECORD_BACKGROUND_AUDIO"; + field public static final String REMAP_MODIFIER_KEYS = "android.permission.REMAP_MODIFIER_KEYS"; field public static final String REMOVE_TASKS = "android.permission.REMOVE_TASKS"; field public static final String REQUEST_UNIQUE_ID_ATTESTATION = "android.permission.REQUEST_UNIQUE_ID_ATTESTATION"; field public static final String RESET_APP_ERRORS = "android.permission.RESET_APP_ERRORS"; @@ -1294,8 +1295,11 @@ package android.hardware.input { public final class InputManager { method public void addUniqueIdAssociation(@NonNull String, @NonNull String); + method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void clearAllModifierKeyRemappings(); method @Nullable public String getCurrentKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier); method @NonNull public java.util.List getKeyboardLayoutDescriptorsForInputDevice(@NonNull android.view.InputDevice); + method @NonNull @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public java.util.Map getModifierKeyRemapping(); + method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void remapModifierKey(int, int); method @RequiresPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT) public void removeKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier, @NonNull String); method public void removeUniqueIdAssociation(@NonNull String); method @RequiresPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT) public void setCurrentKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier, @NonNull String); diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index b26c0a289c21c..eef0f421d29f2 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -123,6 +123,22 @@ interface IInputManager { String[] getKeyboardLayoutListForInputDevice(in InputDeviceIdentifier identifier, int userId, in InputMethodInfo imeInfo, in InputMethodSubtype imeSubtype); + // Modifier key remapping APIs. + @EnforcePermission("REMAP_MODIFIER_KEYS") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.REMAP_MODIFIER_KEYS)") + void remapModifierKey(int fromKey, int toKey); + + @EnforcePermission("REMAP_MODIFIER_KEYS") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.REMAP_MODIFIER_KEYS)") + void clearAllModifierKeyRemappings(); + + @EnforcePermission("REMAP_MODIFIER_KEYS") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.REMAP_MODIFIER_KEYS)") + Map getModifierKeyRemapping(); + // Registers an input devices changed listener. void registerInputDevicesChangedListener(IInputDevicesChangedListener listener); diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index cea3fa1a7fb0d..373541734be0a 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -78,6 +78,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.Executor; @@ -253,6 +254,31 @@ public final class InputManager { }) public @interface SwitchState {} + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "REMAPPABLE_MODIFIER_KEY_" }, value = { + RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_CTRL_LEFT, + RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_CTRL_RIGHT, + RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_META_LEFT, + RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_META_RIGHT, + RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_ALT_LEFT, + RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_ALT_RIGHT, + RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_SHIFT_LEFT, + RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_SHIFT_RIGHT, + RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_CAPS_LOCK, + }) + public @interface RemappableModifierKey { + int REMAPPABLE_MODIFIER_KEY_CTRL_LEFT = KeyEvent.KEYCODE_CTRL_LEFT; + int REMAPPABLE_MODIFIER_KEY_CTRL_RIGHT = KeyEvent.KEYCODE_CTRL_RIGHT; + int REMAPPABLE_MODIFIER_KEY_META_LEFT = KeyEvent.KEYCODE_META_LEFT; + int REMAPPABLE_MODIFIER_KEY_META_RIGHT = KeyEvent.KEYCODE_META_RIGHT; + int REMAPPABLE_MODIFIER_KEY_ALT_LEFT = KeyEvent.KEYCODE_ALT_LEFT; + int REMAPPABLE_MODIFIER_KEY_ALT_RIGHT = KeyEvent.KEYCODE_ALT_RIGHT; + int REMAPPABLE_MODIFIER_KEY_SHIFT_LEFT = KeyEvent.KEYCODE_SHIFT_LEFT; + int REMAPPABLE_MODIFIER_KEY_SHIFT_RIGHT = KeyEvent.KEYCODE_SHIFT_RIGHT; + int REMAPPABLE_MODIFIER_KEY_CAPS_LOCK = KeyEvent.KEYCODE_CAPS_LOCK; + } + /** * Switch State: Unknown. * @@ -853,6 +879,60 @@ public final class InputManager { } } + /** + * Remaps modifier keys. Remapping a modifier key to itself will clear any previous remappings + * for that key. + * + * @param fromKey The modifier key getting remapped. + * @param toKey The modifier key that it is remapped to. + * + * @hide + */ + @TestApi + @RequiresPermission(Manifest.permission.REMAP_MODIFIER_KEYS) + public void remapModifierKey(@RemappableModifierKey int fromKey, + @RemappableModifierKey int toKey) { + try { + mIm.remapModifierKey(fromKey, toKey); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + + /** + * Clears all existing modifier key remappings + * + * @hide + */ + @TestApi + @RequiresPermission(Manifest.permission.REMAP_MODIFIER_KEYS) + public void clearAllModifierKeyRemappings() { + try { + mIm.clearAllModifierKeyRemappings(); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + + /** + * Provides the current modifier key remapping + * + * @return a {fromKey, toKey} map that contains the existing modifier key remappings.. + * {@link RemappableModifierKey} + * + * @hide + */ + @TestApi + @NonNull + @RequiresPermission(Manifest.permission.REMAP_MODIFIER_KEYS) + public Map getModifierKeyRemapping() { + try { + return mIm.getModifierKeyRemapping(); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + /** * Gets the TouchCalibration applied to the specified input device's coordinates. * diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 8da91ee4228f2..9e9b41c3b59d9 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6764,6 +6764,14 @@ @hide --> + + + + + + + diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 81d782e9b8637..0da04a22d31f6 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -294,6 +294,9 @@ public class InputManagerService extends IInputManager.Stub // Manages Keyboard backlight private final KeyboardBacklightController mKeyboardBacklightController; + // Manages Keyboard modifier keys remapping + private final KeyRemapper mKeyRemapper; + // Maximum number of milliseconds to wait for input event injection. private static final int INJECTION_TIMEOUT_MILLIS = 30 * 1000; @@ -408,6 +411,7 @@ public class InputManagerService extends IInputManager.Stub mBatteryController = new BatteryController(mContext, mNative, injector.getLooper()); mKeyboardBacklightController = new KeyboardBacklightController(mContext, mNative, mDataStore, injector.getLooper()); + mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper()); mUseDevInputEventForAudioJack = mContext.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack); @@ -536,6 +540,7 @@ public class InputManagerService extends IInputManager.Stub mKeyboardLayoutManager.systemRunning(); mBatteryController.systemRunning(); mKeyboardBacklightController.systemRunning(); + mKeyRemapper.systemRunning(); } private void reloadDeviceAliases() { @@ -2738,6 +2743,27 @@ public class InputManagerService extends IInputManager.Stub return mKeyboardLayoutManager.getKeyboardLayoutOverlay(identifier); } + @EnforcePermission(Manifest.permission.REMAP_MODIFIER_KEYS) + @Override // Binder call + public void remapModifierKey(int fromKey, int toKey) { + super.remapModifierKey_enforcePermission(); + mKeyRemapper.remapKey(fromKey, toKey); + } + + @EnforcePermission(Manifest.permission.REMAP_MODIFIER_KEYS) + @Override // Binder call + public void clearAllModifierKeyRemappings() { + super.clearAllModifierKeyRemappings_enforcePermission(); + mKeyRemapper.clearAllKeyRemappings(); + } + + @EnforcePermission(Manifest.permission.REMAP_MODIFIER_KEYS) + @Override // Binder call + public Map getModifierKeyRemapping() { + super.getModifierKeyRemapping_enforcePermission(); + return mKeyRemapper.getKeyRemapping(); + } + // Native callback. @SuppressWarnings("unused") private String getDeviceAlias(String uniqueId) { diff --git a/services/core/java/com/android/server/input/KeyRemapper.java b/services/core/java/com/android/server/input/KeyRemapper.java new file mode 100644 index 0000000000000..950e094a205d6 --- /dev/null +++ b/services/core/java/com/android/server/input/KeyRemapper.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input; + +import android.content.Context; +import android.hardware.input.InputManager; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.view.InputDevice; + +import com.android.internal.annotations.GuardedBy; + +import java.util.Map; +import java.util.Objects; + +/** + * A component of {@link InputManagerService} responsible for managing key remappings. + * + * @hide + */ +final class KeyRemapper implements InputManager.InputDeviceListener { + + private static final int MSG_UPDATE_EXISTING_DEVICES = 1; + private static final int MSG_REMAP_KEY = 2; + private static final int MSG_CLEAR_ALL_REMAPPING = 3; + + private final Context mContext; + private final NativeInputManagerService mNative; + // The PersistentDataStore should be locked before use. + @GuardedBy("mDataStore") + private final PersistentDataStore mDataStore; + private final Handler mHandler; + + KeyRemapper(Context context, NativeInputManagerService nativeService, + PersistentDataStore dataStore, Looper looper) { + mContext = context; + mNative = nativeService; + mDataStore = dataStore; + mHandler = new Handler(looper, this::handleMessage); + } + + public void systemRunning() { + InputManager inputManager = Objects.requireNonNull( + mContext.getSystemService(InputManager.class)); + inputManager.registerInputDeviceListener(this, mHandler); + + Message msg = Message.obtain(mHandler, MSG_UPDATE_EXISTING_DEVICES, + inputManager.getInputDeviceIds()); + mHandler.sendMessage(msg); + } + + public void remapKey(int fromKey, int toKey) { + Message msg = Message.obtain(mHandler, MSG_REMAP_KEY, fromKey, toKey); + mHandler.sendMessage(msg); + } + + public void clearAllKeyRemappings() { + Message msg = Message.obtain(mHandler, MSG_CLEAR_ALL_REMAPPING); + mHandler.sendMessage(msg); + } + + public Map getKeyRemapping() { + synchronized (mDataStore) { + return mDataStore.getKeyRemapping(); + } + } + + private void addKeyRemapping(int fromKey, int toKey) { + InputManager inputManager = Objects.requireNonNull( + mContext.getSystemService(InputManager.class)); + for (int deviceId : inputManager.getInputDeviceIds()) { + InputDevice inputDevice = inputManager.getInputDevice(deviceId); + if (inputDevice != null && !inputDevice.isVirtual() && inputDevice.isFullKeyboard()) { + mNative.addKeyRemapping(deviceId, fromKey, toKey); + } + } + } + + private void remapKeyInternal(int fromKey, int toKey) { + addKeyRemapping(fromKey, toKey); + synchronized (mDataStore) { + try { + if (fromKey == toKey) { + mDataStore.clearMappedKey(fromKey); + } else { + mDataStore.remapKey(fromKey, toKey); + } + } finally { + mDataStore.saveIfNeeded(); + } + } + } + + private void clearAllRemappingsInternal() { + synchronized (mDataStore) { + try { + Map keyRemapping = mDataStore.getKeyRemapping(); + for (int fromKey : keyRemapping.keySet()) { + mDataStore.clearMappedKey(fromKey); + + // Remapping to itself will clear the remapping on native side + addKeyRemapping(fromKey, fromKey); + } + } finally { + mDataStore.saveIfNeeded(); + } + } + } + + @Override + public void onInputDeviceAdded(int deviceId) { + InputManager inputManager = Objects.requireNonNull( + mContext.getSystemService(InputManager.class)); + InputDevice inputDevice = inputManager.getInputDevice(deviceId); + if (inputDevice != null && !inputDevice.isVirtual() && inputDevice.isFullKeyboard()) { + Map remapping = getKeyRemapping(); + remapping.forEach( + (fromKey, toKey) -> mNative.addKeyRemapping(deviceId, fromKey, toKey)); + } + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + } + + @Override + public void onInputDeviceChanged(int deviceId) { + } + + private boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_UPDATE_EXISTING_DEVICES: + for (int deviceId : (int[]) msg.obj) { + onInputDeviceAdded(deviceId); + } + return true; + case MSG_REMAP_KEY: + remapKeyInternal(msg.arg1, msg.arg2); + return true; + case MSG_CLEAR_ALL_REMAPPING: + clearAllRemappingsInternal(); + return true; + } + return false; + } +} diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index cfa7fb141be19..8781c6e2b934f 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -47,6 +47,8 @@ interface NativeInputManagerService { int getSwitchState(int deviceId, int sourceMask, int sw); + void addKeyRemapping(int deviceId, int fromKeyCode, int toKeyCode); + boolean hasKeys(int deviceId, int sourceMask, int[] keyCodes, boolean[] keyExists); int getKeyCodeForKeyLocation(int deviceId, int locationKeyCode); @@ -234,6 +236,9 @@ interface NativeInputManagerService { @Override public native int getSwitchState(int deviceId, int sourceMask, int sw); + @Override + public native void addKeyRemapping(int deviceId, int fromKeyCode, int toKeyCode); + @Override public native boolean hasKeys(int deviceId, int sourceMask, int[] keyCodes, boolean[] keyExists); diff --git a/services/core/java/com/android/server/input/PersistentDataStore.java b/services/core/java/com/android/server/input/PersistentDataStore.java index 1bb10c710839f..375377a7510d8 100644 --- a/services/core/java/com/android/server/input/PersistentDataStore.java +++ b/services/core/java/com/android/server/input/PersistentDataStore.java @@ -27,14 +27,13 @@ import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.XmlUtils; +import com.android.modules.utils.TypedXmlPullParser; +import com.android.modules.utils.TypedXmlSerializer; import libcore.io.IoUtils; import org.xmlpull.v1.XmlPullParserException; -import com.android.modules.utils.TypedXmlPullParser; -import com.android.modules.utils.TypedXmlSerializer; - import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -64,6 +63,8 @@ import java.util.Set; final class PersistentDataStore { static final String TAG = "InputManager"; + private static final int INVALID_VALUE = -1; + // Input device state by descriptor. private final HashMap mInputDevices = new HashMap(); @@ -77,6 +78,9 @@ final class PersistentDataStore { // True if there are changes to be saved. private boolean mDirty; + // Storing key remapping + private Map mKeyRemapping = new HashMap<>(); + public PersistentDataStore() { this(new Injector()); } @@ -187,6 +191,30 @@ final class PersistentDataStore { return state.getKeyboardBacklightBrightness(lightId); } + public boolean remapKey(int fromKey, int toKey) { + loadIfNeeded(); + if (mKeyRemapping.getOrDefault(fromKey, INVALID_VALUE) == toKey) { + return false; + } + mKeyRemapping.put(fromKey, toKey); + setDirty(); + return true; + } + + public boolean clearMappedKey(int key) { + loadIfNeeded(); + if (mKeyRemapping.containsKey(key)) { + mKeyRemapping.remove(key); + setDirty(); + } + return true; + } + + public Map getKeyRemapping() { + loadIfNeeded(); + return new HashMap<>(mKeyRemapping); + } + public boolean removeUninstalledKeyboardLayouts(Set availableKeyboardLayouts) { boolean changed = false; for (InputDeviceState state : mInputDevices.values()) { @@ -229,6 +257,7 @@ final class PersistentDataStore { } private void clearState() { + mKeyRemapping.clear(); mInputDevices.clear(); } @@ -280,7 +309,9 @@ final class PersistentDataStore { XmlUtils.beginDocument(parser, "input-manager-state"); final int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { - if (parser.getName().equals("input-devices")) { + if (parser.getName().equals("key-remapping")) { + loadKeyRemappingFromXml(parser); + } else if (parser.getName().equals("input-devices")) { loadInputDevicesFromXml(parser); } } @@ -307,10 +338,31 @@ final class PersistentDataStore { } } + private void loadKeyRemappingFromXml(TypedXmlPullParser parser) + throws IOException, XmlPullParserException { + final int outerDepth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, outerDepth)) { + if (parser.getName().equals("remap")) { + int fromKey = parser.getAttributeInt(null, "from-key"); + int toKey = parser.getAttributeInt(null, "to-key"); + mKeyRemapping.put(fromKey, toKey); + } + } + } + private void saveToXml(TypedXmlSerializer serializer) throws IOException { serializer.startDocument(null, true); serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); serializer.startTag(null, "input-manager-state"); + serializer.startTag(null, "key-remapping"); + for (int fromKey : mKeyRemapping.keySet()) { + int toKey = mKeyRemapping.get(fromKey); + serializer.startTag(null, "remap"); + serializer.attributeInt(null, "from-key", fromKey); + serializer.attributeInt(null, "to-key", toKey); + serializer.endTag(null, "remap"); + } + serializer.endTag(null, "key-remapping"); serializer.startTag(null, "input-devices"); for (Map.Entry entry : mInputDevices.entrySet()) { final String descriptor = entry.getKey(); @@ -329,7 +381,6 @@ final class PersistentDataStore { private static final String[] CALIBRATION_NAME = { "x_scale", "x_ymix", "x_offset", "y_xmix", "y_scale", "y_offset" }; - private static final int INVALID_VALUE = -1; private final TouchCalibration[] mTouchCalibration = new TouchCalibration[4]; @Nullable private String mCurrentKeyboardLayout; diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 969056e7c8b41..145e0885b105f 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -1578,6 +1578,12 @@ static std::vector getIntArray(JNIEnv* env, jintArray arr) { return vec; } +static void nativeAddKeyRemapping(JNIEnv* env, jobject nativeImplObj, jint deviceId, + jint fromKeyCode, jint toKeyCode) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + im->getInputManager()->getReader().addKeyRemapping(deviceId, fromKeyCode, toKeyCode); +} + static jboolean nativeHasKeys(JNIEnv* env, jobject nativeImplObj, jint deviceId, jint sourceMask, jintArray keyCodes, jbooleanArray outFlags) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); @@ -2360,6 +2366,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"getScanCodeState", "(III)I", (void*)nativeGetScanCodeState}, {"getKeyCodeState", "(III)I", (void*)nativeGetKeyCodeState}, {"getSwitchState", "(III)I", (void*)nativeGetSwitchState}, + {"addKeyRemapping", "(III)V", (void*)nativeAddKeyRemapping}, {"hasKeys", "(II[I[Z)Z", (void*)nativeHasKeys}, {"getKeyCodeForKeyLocation", "(II)I", (void*)nativeGetKeyCodeForKeyLocation}, {"createInputChannel", "(Ljava/lang/String;)Landroid/view/InputChannel;", diff --git a/services/tests/servicestests/src/com/android/server/input/KeyRemapperTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyRemapperTests.kt new file mode 100644 index 0000000000000..c22782c91bdf4 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/input/KeyRemapperTests.kt @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input + +import android.content.Context +import android.content.ContextWrapper +import android.hardware.input.IInputManager +import android.hardware.input.InputManager +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import android.view.InputDevice +import android.view.KeyEvent +import androidx.test.core.app.ApplicationProvider +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.junit.MockitoJUnit +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream + +private fun createKeyboard(deviceId: Int): InputDevice = + InputDevice.Builder() + .setId(deviceId) + .setName("Device $deviceId") + .setDescriptor("descriptor $deviceId") + .setSources(InputDevice.SOURCE_KEYBOARD) + .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC) + .setExternal(true) + .build() + +/** + * Tests for {@link KeyRemapper}. + * + * Build/Install/Run: + * atest FrameworksServicesTests:KeyRemapperTests + */ +@Presubmit +class KeyRemapperTests { + + companion object { + const val DEVICE_ID = 1 + val REMAPPABLE_KEYS = intArrayOf( + KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT, + KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_META_RIGHT, + KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_RIGHT, + KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT, + KeyEvent.KEYCODE_CAPS_LOCK + ) + } + + @get:Rule + val rule = MockitoJUnit.rule()!! + + @Mock + private lateinit var iInputManager: IInputManager + @Mock + private lateinit var native: NativeInputManagerService + private lateinit var mKeyRemapper: KeyRemapper + private lateinit var context: Context + private lateinit var dataStore: PersistentDataStore + private lateinit var testLooper: TestLooper + + @Before + fun setup() { + context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + dataStore = PersistentDataStore(object : PersistentDataStore.Injector() { + override fun openRead(): InputStream? { + throw FileNotFoundException() + } + + override fun startWrite(): FileOutputStream? { + throw IOException() + } + + override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} + }) + testLooper = TestLooper() + mKeyRemapper = KeyRemapper( + context, + native, + dataStore, + testLooper.looper + ) + val inputManager = InputManager.resetInstance(iInputManager) + Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) + .thenReturn(inputManager) + Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID)) + } + + @After + fun tearDown() { + InputManager.clearInstance() + } + + @Test + fun testKeyRemapping() { + val keyboard = createKeyboard(DEVICE_ID) + Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboard) + + for (i in REMAPPABLE_KEYS.indices) { + val fromKeyCode = REMAPPABLE_KEYS[i] + val toKeyCode = REMAPPABLE_KEYS[(i + 1) % REMAPPABLE_KEYS.size] + mKeyRemapper.remapKey(fromKeyCode, toKeyCode) + testLooper.dispatchNext() + } + + val remapping = mKeyRemapper.keyRemapping + val expectedSize = REMAPPABLE_KEYS.size + assertEquals("Remapping size should be $expectedSize", expectedSize, remapping.size) + + for (i in REMAPPABLE_KEYS.indices) { + val fromKeyCode = REMAPPABLE_KEYS[i] + val toKeyCode = REMAPPABLE_KEYS[(i + 1) % REMAPPABLE_KEYS.size] + assertEquals( + "Remapping should include mapping from $fromKeyCode to $toKeyCode", + toKeyCode, + remapping.getOrDefault(fromKeyCode, -1) + ) + } + + mKeyRemapper.clearAllKeyRemappings() + testLooper.dispatchNext() + + assertEquals( + "Remapping size should be 0 after clearAllModifierKeyRemappings", + 0, + mKeyRemapper.keyRemapping.size + ) + } +} \ No newline at end of file