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
This commit is contained in:
Vaibhav Devmurari
2022-10-10 16:50:16 +00:00
parent 91618c202d
commit a7779bf662
11 changed files with 516 additions and 5 deletions

View File

@@ -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<java.lang.String> getKeyboardLayoutDescriptorsForInputDevice(@NonNull android.view.InputDevice);
method @NonNull @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public java.util.Map<java.lang.Integer,java.lang.Integer> 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);

View File

@@ -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);

View File

@@ -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<Integer, Integer> getModifierKeyRemapping() {
try {
return mIm.getModifierKeyRemapping();
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
/**
* Gets the TouchCalibration applied to the specified input device's coordinates.
*

View File

@@ -6764,6 +6764,14 @@
@hide -->
<permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART"
android:protectionLevel="signature" />
<!-- Allows low-level access to re-mapping modifier keys.
<p>Not for use by third-party applications.
@hide
@TestApi -->
<permission android:name="android.permission.REMAP_MODIFIER_KEYS"
android:protectionLevel="signature" />
<uses-permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART" />
<!-- Allows financed device kiosk apps to perform actions on the Device Lock service

View File

@@ -774,6 +774,9 @@
<!-- Permissions required for CTS test - CtsAppFgsTestCases -->
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<!-- Permission required for CTS test - CtsHardwareTestCases -->
<uses-permission android:name="android.permission.REMAP_MODIFIER_KEYS" />
<!-- Permissions required for CTS test - CtsAppFgsTestCases -->
<uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED" />
<uses-permission android:name="android.permission.health.READ_BASAL_BODY_TEMPERATURE" />

View File

@@ -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<Integer, Integer> getModifierKeyRemapping() {
super.getModifierKeyRemapping_enforcePermission();
return mKeyRemapper.getKeyRemapping();
}
// Native callback.
@SuppressWarnings("unused")
private String getDeviceAlias(String uniqueId) {

View File

@@ -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<Integer, Integer> 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<Integer, Integer> 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<Integer, Integer> 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;
}
}

View File

@@ -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);

View File

@@ -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<String, InputDeviceState> mInputDevices =
new HashMap<String, InputDeviceState>();
@@ -77,6 +78,9 @@ final class PersistentDataStore {
// True if there are changes to be saved.
private boolean mDirty;
// Storing key remapping
private Map<Integer, Integer> 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<Integer, Integer> getKeyRemapping() {
loadIfNeeded();
return new HashMap<>(mKeyRemapping);
}
public boolean removeUninstalledKeyboardLayouts(Set<String> 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<String, InputDeviceState> 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;

View File

@@ -1578,6 +1578,12 @@ static std::vector<int32_t> 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;",

View File

@@ -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
)
}
}