Merge "Add internal APIs to support modifier key remapping"
This commit is contained in:
committed by
Android (Google) Code Review
commit
04625c2cc7
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -6776,6 +6776,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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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) {
|
||||
|
||||
161
services/core/java/com/android/server/input/KeyRemapper.java
Normal file
161
services/core/java/com/android/server/input/KeyRemapper.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;",
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user