From 5892a47dbef230618360f7d98d055b3759127d81 Mon Sep 17 00:00:00 2001 From: Zixuan Qu Date: Mon, 14 Nov 2022 05:36:45 +0000 Subject: [PATCH] Specify language tag and layout type when creating virtual keyboard. 1) Added languageTag in VirtualKeyboardConfig which holds a BCP 47 language tag. 2) Added layoutType in VIrtualKeyboardConfig which holds the type of the layout such "qwerty". 3) Pass the above layout info to native input reader in a similary way that display ID association is handled. Bug: 237537306 Test: atest VirtualKeyboardTest, atest inputflinger_tests Change-Id: Ib37f4785e6c087d58d7b54565ddbaf257a6c0871 --- core/api/system-current.txt | 6 ++ .../hardware/input/VirtualKeyboardConfig.java | 89 +++++++++++++++++ core/java/android/view/InputDevice.java | 96 +++++++++++++++---- core/jni/android_view_InputDevice.cpp | 23 ++++- .../companion/virtual/InputController.java | 21 ++-- .../companion/virtual/VirtualDeviceImpl.java | 3 +- .../server/input/InputManagerInternal.java | 20 ++++ .../server/input/InputManagerService.java | 52 +++++++++- .../input/NativeInputManagerService.java | 5 + ...droid_server_input_InputManagerService.cpp | 46 +++++++-- .../virtual/InputControllerTest.java | 17 +++- .../VirtualDeviceManagerServiceTest.java | 2 + .../server/input/InputManagerServiceTests.kt | 18 +++- .../android/test/input/InputDeviceTest.java | 4 + 14 files changed, 352 insertions(+), 50 deletions(-) diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 566ac4535b0d1..b33f359f79582 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -4705,13 +4705,19 @@ package android.hardware.input { public final class VirtualKeyboardConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable { method public int describeContents(); + method @NonNull public String getLanguageTag(); + method @NonNull public String getLayoutType(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator CREATOR; + field public static final String DEFAULT_LANGUAGE_TAG = "en-Latn-US"; + field public static final String DEFAULT_LAYOUT_TYPE = "qwerty"; } public static final class VirtualKeyboardConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder { ctor public VirtualKeyboardConfig.Builder(); method @NonNull public android.hardware.input.VirtualKeyboardConfig build(); + method @NonNull public android.hardware.input.VirtualKeyboardConfig.Builder setLanguageTag(@NonNull String); + method @NonNull public android.hardware.input.VirtualKeyboardConfig.Builder setLayoutType(@NonNull String); } public class VirtualMouse implements java.io.Closeable { diff --git a/core/java/android/hardware/input/VirtualKeyboardConfig.java b/core/java/android/hardware/input/VirtualKeyboardConfig.java index 94638574c82d3..28d8a0ff553ec 100644 --- a/core/java/android/hardware/input/VirtualKeyboardConfig.java +++ b/core/java/android/hardware/input/VirtualKeyboardConfig.java @@ -18,9 +18,12 @@ package android.hardware.input; import android.annotation.NonNull; import android.annotation.SystemApi; +import android.icu.util.ULocale; import android.os.Parcel; import android.os.Parcelable; +import java.util.Objects; + /** * Configurations to create virtual keyboard. @@ -29,6 +32,18 @@ import android.os.Parcelable; */ @SystemApi public final class VirtualKeyboardConfig extends VirtualInputDeviceConfig implements Parcelable { + /** + * Default language tag when creating virtual keyboard. Used when the language tag is not set. + */ + public static final String DEFAULT_LANGUAGE_TAG = "en-Latn-US"; + /** Default layout type when creating virtual keyboard. Used when the layout type is not set. */ + public static final String DEFAULT_LAYOUT_TYPE = "qwerty"; + + @NonNull + private final String mLanguageTag; + + @NonNull + private final String mLayoutType; @NonNull public static final Creator CREATOR = @@ -46,10 +61,30 @@ public final class VirtualKeyboardConfig extends VirtualInputDeviceConfig implem private VirtualKeyboardConfig(@NonNull Builder builder) { super(builder); + mLanguageTag = builder.mLanguageTag; + mLayoutType = builder.mLayoutType; } private VirtualKeyboardConfig(@NonNull Parcel in) { super(in); + mLanguageTag = in.readString8(); + mLayoutType = in.readString8(); + } + + /** + * @see Builder#setLanguageTag(). + */ + @NonNull + public String getLanguageTag() { + return mLanguageTag; + } + + /** + * @see Builder#setLayoutType(). + */ + @NonNull + public String getLayoutType() { + return mLayoutType; } @Override @@ -60,12 +95,66 @@ public final class VirtualKeyboardConfig extends VirtualInputDeviceConfig implem @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); + dest.writeString8(mLanguageTag); + dest.writeString8(mLayoutType); } /** * Builder for creating a {@link VirtualKeyboardConfig}. */ public static final class Builder extends VirtualInputDeviceConfig.Builder { + @NonNull + private String mLanguageTag = DEFAULT_LANGUAGE_TAG; + @NonNull + private String mLayoutType = DEFAULT_LAYOUT_TYPE; + + /** + * Sets the preferred input language of the virtual keyboard using an IETF + * BCP-47 + * conformant tag. See {@code keyboardLocale} attribute in + * frameworks/base/packages/InputDevices/res/xml/keyboard_layouts.xml for a list of + * supported language tags. + * + * The passed in {@code languageTag} will be canonized using {@link + * ULocale} and used by the system as a hint to configure the keyboard layout. + * + * If {@code languageTag} is not specified, the virtual keyboard will be created with {@link + * #DEFAULT_LANGUAGE_TAG}. + * + * Note that the preferred layout is not guaranteed. If the specified language is + * well-formed but not supported, the keyboard will be using English US QWERTY layout. + * + * @throws IllegalArgumentException if either of the language or country is not present in + * the language tag. + */ + @NonNull + public Builder setLanguageTag(@NonNull String languageTag) { + Objects.requireNonNull(languageTag, "languageTag cannot be null"); + ULocale locale = ULocale.forLanguageTag(languageTag); + if (locale.getLanguage().isEmpty() || locale.getCountry().isEmpty()) { + throw new IllegalArgumentException("The language tag is not valid."); + } + mLanguageTag = ULocale.createCanonical(locale).toLanguageTag(); + return this; + } + + /** + * Sets the preferred layout type of the virtual keyboard. See {@code keyboardLayoutType} + * attribute in frameworks/base/packages/InputDevices/res/xml/keyboard_layouts.xml for a + * list of supported layout types. + * + * Note that the preferred layout is not guaranteed. If the specified layout type is + * well-formed but not supported, the keyboard will be using English US QWERTY layout. + * + * If not specified, the virtual keyboard will be created with {@link #DEFAULT_LAYOUT_TYPE}. + */ + @NonNull + public Builder setLayoutType(@NonNull String layoutType) { + Objects.requireNonNull(layoutType, "layoutType cannot be null"); + mLayoutType = layoutType; + return this; + } + /** * Builds the {@link VirtualKeyboardConfig} instance. */ diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 3d7843c06c6a0..89c7a360a91cc 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -30,6 +30,7 @@ import android.hardware.input.InputDeviceCountryCode; import android.hardware.input.InputDeviceIdentifier; import android.hardware.input.InputManager; import android.hardware.lights.LightsManager; +import android.icu.util.ULocale; import android.os.Build; import android.os.NullVibrator; import android.os.Parcel; @@ -76,6 +77,10 @@ public final class InputDevice implements Parcelable { private final KeyCharacterMap mKeyCharacterMap; @InputDeviceCountryCode private final int mCountryCode; + @Nullable + private final String mKeyboardLanguageTag; + @Nullable + private final String mKeyboardLayoutType; private final boolean mHasVibrator; private final boolean mHasMicrophone; private final boolean mHasButtonUnderPad; @@ -464,6 +469,7 @@ public final class InputDevice implements Parcelable { private InputDevice(int id, int generation, int controllerNumber, String name, int vendorId, int productId, String descriptor, boolean isExternal, int sources, int keyboardType, KeyCharacterMap keyCharacterMap, @InputDeviceCountryCode int countryCode, + @Nullable String keyboardLanguageTag, @Nullable String keyboardLayoutType, boolean hasVibrator, boolean hasMicrophone, boolean hasButtonUnderPad, boolean hasSensor, boolean hasBattery, boolean supportsUsi) { mId = id; @@ -478,6 +484,14 @@ public final class InputDevice implements Parcelable { mKeyboardType = keyboardType; mKeyCharacterMap = keyCharacterMap; mCountryCode = countryCode; + if (keyboardLanguageTag != null) { + mKeyboardLanguageTag = ULocale + .createCanonical(ULocale.forLanguageTag(keyboardLanguageTag)) + .toLanguageTag(); + } else { + mKeyboardLanguageTag = null; + } + mKeyboardLayoutType = keyboardLayoutType; mHasVibrator = hasVibrator; mHasMicrophone = hasMicrophone; mHasButtonUnderPad = hasButtonUnderPad; @@ -500,6 +514,8 @@ public final class InputDevice implements Parcelable { mSources = in.readInt(); mKeyboardType = in.readInt(); mCountryCode = in.readInt(); + mKeyboardLanguageTag = in.readString8(); + mKeyboardLayoutType = in.readString8(); mHasVibrator = in.readInt() != 0; mHasMicrophone = in.readInt() != 0; mHasButtonUnderPad = in.readInt() != 0; @@ -521,6 +537,7 @@ public final class InputDevice implements Parcelable { /** * InputDevice builder used to create an InputDevice for tests in Java. + * * @hide */ @VisibleForTesting @@ -543,111 +560,125 @@ public final class InputDevice implements Parcelable { private boolean mHasBattery = false; @InputDeviceCountryCode private int mCountryCode = InputDeviceCountryCode.INVALID; + private String mKeyboardLanguageTag = null; + private String mKeyboardLayoutType = null; private boolean mSupportsUsi = false; - /** @see InputDevice#getId() */ + /** @see InputDevice#getId() */ public Builder setId(int id) { mId = id; return this; } - /** @see InputDevice#getGeneration() */ + /** @see InputDevice#getGeneration() */ public Builder setGeneration(int generation) { mGeneration = generation; return this; } - /** @see InputDevice#getControllerNumber() */ + /** @see InputDevice#getControllerNumber() */ public Builder setControllerNumber(int controllerNumber) { mControllerNumber = controllerNumber; return this; } - /** @see InputDevice#getName() */ + /** @see InputDevice#getName() */ public Builder setName(String name) { mName = name; return this; } - /** @see InputDevice#getVendorId() */ + /** @see InputDevice#getVendorId() */ public Builder setVendorId(int vendorId) { mVendorId = vendorId; return this; } - /** @see InputDevice#getProductId() */ + /** @see InputDevice#getProductId() */ public Builder setProductId(int productId) { mProductId = productId; return this; } - /** @see InputDevice#getDescriptor() */ + /** @see InputDevice#getDescriptor() */ public Builder setDescriptor(String descriptor) { mDescriptor = descriptor; return this; } - /** @see InputDevice#isExternal() */ + /** @see InputDevice#isExternal() */ public Builder setExternal(boolean external) { mIsExternal = external; return this; } - /** @see InputDevice#getSources() */ + /** @see InputDevice#getSources() */ public Builder setSources(int sources) { mSources = sources; return this; } - /** @see InputDevice#getKeyboardType() */ + /** @see InputDevice#getKeyboardType() */ public Builder setKeyboardType(int keyboardType) { mKeyboardType = keyboardType; return this; } - /** @see InputDevice#getKeyCharacterMap() */ + /** @see InputDevice#getKeyCharacterMap() */ public Builder setKeyCharacterMap(KeyCharacterMap keyCharacterMap) { mKeyCharacterMap = keyCharacterMap; return this; } - /** @see InputDevice#getVibrator() */ + /** @see InputDevice#getVibrator() */ public Builder setHasVibrator(boolean hasVibrator) { mHasVibrator = hasVibrator; return this; } - /** @see InputDevice#hasMicrophone() */ + /** @see InputDevice#hasMicrophone() */ public Builder setHasMicrophone(boolean hasMicrophone) { mHasMicrophone = hasMicrophone; return this; } - /** @see InputDevice#hasButtonUnderPad() */ + /** @see InputDevice#hasButtonUnderPad() */ public Builder setHasButtonUnderPad(boolean hasButtonUnderPad) { mHasButtonUnderPad = hasButtonUnderPad; return this; } - /** @see InputDevice#hasSensor() */ + /** @see InputDevice#hasSensor() */ public Builder setHasSensor(boolean hasSensor) { mHasSensor = hasSensor; return this; } - /** @see InputDevice#hasBattery() */ + /** @see InputDevice#hasBattery() */ public Builder setHasBattery(boolean hasBattery) { mHasBattery = hasBattery; return this; } - /** @see InputDevice#getCountryCode() */ + /** @see InputDevice#getCountryCode() */ public Builder setCountryCode(@InputDeviceCountryCode int countryCode) { mCountryCode = countryCode; return this; } - /** @see InputDevice#supportsUsi() () */ + /** @see InputDevice#getKeyboardLanguageTag() */ + public Builder setKeyboardLanguageTag(String keyboardLanguageTag) { + mKeyboardLanguageTag = keyboardLanguageTag; + return this; + } + + /** @see InputDevice#getKeyboardLayoutType() */ + public Builder setKeyboardLayoutType(String keyboardLayoutType) { + mKeyboardLayoutType = keyboardLayoutType; + return this; + } + + /** @see InputDevice#supportsUsi() () */ public Builder setSupportsUsi(boolean supportsUsi) { mSupportsUsi = supportsUsi; return this; @@ -657,8 +688,8 @@ public final class InputDevice implements Parcelable { public InputDevice build() { return new InputDevice(mId, mGeneration, mControllerNumber, mName, mVendorId, mProductId, mDescriptor, mIsExternal, mSources, mKeyboardType, mKeyCharacterMap, - mCountryCode, mHasVibrator, mHasMicrophone, mHasButtonUnderPad, mHasSensor, - mHasBattery, mSupportsUsi); + mCountryCode, mKeyboardLanguageTag, mKeyboardLayoutType, mHasVibrator, + mHasMicrophone, mHasButtonUnderPad, mHasSensor, mHasBattery, mSupportsUsi); } } @@ -887,8 +918,31 @@ public final class InputDevice implements Parcelable { return mCountryCode; } + /** + * Returns the keyboard language as an IETF + * BCP-47 + * conformant tag if available. + * + * @hide + */ + @Nullable + public String getKeyboardLanguageTag() { + return mKeyboardLanguageTag; + } + + /** + * Returns the keyboard layout type if available. + * + * @hide + */ + @Nullable + public String getKeyboardLayoutType() { + return mKeyboardLayoutType; + } + /** * Gets whether the device is capable of producing the list of keycodes. + * * @param keys The list of android keycodes to check for. * @return An array of booleans where each member specifies whether the device is capable of * generating the keycode given by the corresponding value at the same index in the keys array. @@ -1340,6 +1394,8 @@ public final class InputDevice implements Parcelable { out.writeInt(mSources); out.writeInt(mKeyboardType); out.writeInt(mCountryCode); + out.writeString8(mKeyboardLanguageTag); + out.writeString8(mKeyboardLayoutType); out.writeInt(mHasVibrator ? 1 : 0); out.writeInt(mHasMicrophone ? 1 : 0); out.writeInt(mHasButtonUnderPad ? 1 : 0); diff --git a/core/jni/android_view_InputDevice.cpp b/core/jni/android_view_InputDevice.cpp index b2994f41af4bc..02f6a77828c6b 100644 --- a/core/jni/android_view_InputDevice.cpp +++ b/core/jni/android_view_InputDevice.cpp @@ -48,6 +48,18 @@ jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& devi return NULL; } + std::optional layoutInfo = deviceInfo.getKeyboardLayoutInfo(); + ScopedLocalRef keyboardLanguageTagObj(env, + env->NewStringUTF( + layoutInfo + ? layoutInfo->languageTag.c_str() + : NULL)); + ScopedLocalRef keyboardLayoutTypeObj(env, + env->NewStringUTF( + layoutInfo + ? layoutInfo->layoutType.c_str() + : NULL)); + ScopedLocalRef kcmObj(env, android_view_KeyCharacterMap_create(env, deviceInfo.getId(), deviceInfo.getKeyCharacterMap())); @@ -66,7 +78,8 @@ jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& devi static_cast(ident.product), descriptorObj.get(), deviceInfo.isExternal(), deviceInfo.getSources(), deviceInfo.getKeyboardType(), kcmObj.get(), - deviceInfo.getCountryCode(), deviceInfo.hasVibrator(), + deviceInfo.getCountryCode(), keyboardLanguageTagObj.get(), + keyboardLayoutTypeObj.get(), deviceInfo.hasVibrator(), deviceInfo.hasMic(), deviceInfo.hasButtonUnderPad(), deviceInfo.hasSensor(), deviceInfo.hasBattery(), deviceInfo.supportsUsi())); @@ -91,10 +104,10 @@ int register_android_view_InputDevice(JNIEnv* env) gInputDeviceClassInfo.clazz = FindClassOrDie(env, "android/view/InputDevice"); gInputDeviceClassInfo.clazz = MakeGlobalRefOrDie(env, gInputDeviceClassInfo.clazz); - gInputDeviceClassInfo.ctor = - GetMethodIDOrDie(env, gInputDeviceClassInfo.clazz, "", - "(IIILjava/lang/String;IILjava/lang/" - "String;ZIILandroid/view/KeyCharacterMap;IZZZZZZ)V"); + gInputDeviceClassInfo.ctor = GetMethodIDOrDie(env, gInputDeviceClassInfo.clazz, "", + "(IIILjava/lang/String;IILjava/lang/" + "String;ZIILandroid/view/KeyCharacterMap;ILjava/" + "lang/String;Ljava/lang/String;ZZZZZZ)V"); gInputDeviceClassInfo.addMotionRange = GetMethodIDOrDie(env, gInputDeviceClassInfo.clazz, "addMotionRange", "(IIFFFFF)V"); diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java index 97b5d6ddf6b62..d5cc58f3cd0cb 100644 --- a/services/companion/java/com/android/server/companion/virtual/InputController.java +++ b/services/companion/java/com/android/server/companion/virtual/InputController.java @@ -142,17 +142,18 @@ class InputController { } } - void createKeyboard(@NonNull String deviceName, - int vendorId, - int productId, - @NonNull IBinder deviceToken, - int displayId) { + void createKeyboard(@NonNull String deviceName, int vendorId, int productId, + @NonNull IBinder deviceToken, int displayId, @NonNull String languageTag, + @NonNull String layoutType) { final String phys = createPhys(PHYS_TYPE_KEYBOARD); + mInputManagerInternal.addKeyboardLayoutAssociation(phys, languageTag, + layoutType); try { createDeviceInternal(InputDeviceDescriptor.TYPE_KEYBOARD, deviceName, vendorId, productId, deviceToken, displayId, phys, () -> mNativeWrapper.openUinputKeyboard(deviceName, vendorId, productId, phys)); } catch (DeviceCreationException e) { + mInputManagerInternal.removeKeyboardLayoutAssociation(phys); throw new RuntimeException( "Failed to create virtual keyboard device '" + deviceName + "'.", e); } @@ -233,12 +234,16 @@ class InputController { InputDeviceDescriptor inputDeviceDescriptor) { token.unlinkToDeath(inputDeviceDescriptor.getDeathRecipient(), /* flags= */ 0); mNativeWrapper.closeUinput(inputDeviceDescriptor.getFileDescriptor()); - - InputManager.getInstance().removeUniqueIdAssociation(inputDeviceDescriptor.getPhys()); + String phys = inputDeviceDescriptor.getPhys(); + InputManager.getInstance().removeUniqueIdAssociation(phys); // Type associations are added in the case of navigation touchpads. Those should be removed // once the input device gets closed. if (inputDeviceDescriptor.getType() == InputDeviceDescriptor.TYPE_NAVIGATION_TOUCHPAD) { - mInputManagerInternal.unsetTypeAssociation(inputDeviceDescriptor.getPhys()); + mInputManagerInternal.unsetTypeAssociation(phys); + } + + if (inputDeviceDescriptor.getType() == InputDeviceDescriptor.TYPE_KEYBOARD) { + mInputManagerInternal.removeKeyboardLayoutAssociation(phys); } // Reset values to the default if all virtual mice are unregistered, or set display diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 12ad9f1cf5805..5ab2d8e88e030 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -435,7 +435,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub final long ident = Binder.clearCallingIdentity(); try { mInputController.createKeyboard(config.getInputDeviceName(), config.getVendorId(), - config.getProductId(), deviceToken, config.getAssociatedDisplayId()); + config.getProductId(), deviceToken, config.getAssociatedDisplayId(), + config.getLanguageTag(), config.getLayoutType()); } finally { Binder.restoreCallingIdentity(ident); } diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index 01a564d6816fc..30f6145f77e3c 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -186,4 +186,24 @@ public abstract class InputManagerInternal { * @param inputPort The port of the input device. */ public abstract void unsetTypeAssociation(@NonNull String inputPort); + + /** + * Add a mapping from the input port and a keyboard layout, by unique id. Input + * ports are expected to be unique. + * + * @param inputPort The port of the input device. + * @param languageTag the language of the input device as an IETF + * BCP-47 + * conformant tag. + * @param layoutType the layout type such as "qwerty" or "azerty". + */ + public abstract void addKeyboardLayoutAssociation(@NonNull String inputPort, + @NonNull String languageTag, @NonNull String layoutType); + + /** + * Removes the mapping from input port to the keyboard layout identifier. + * + * @param inputPort The port of the input device. + */ + public abstract void removeKeyboardLayoutAssociation(@NonNull String inputPort); } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 1809b1821b5e8..e1e99a1cb3b78 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -247,6 +247,11 @@ public class InputManagerService extends IInputManager.Stub private final Map mRuntimeAssociations = new ArrayMap<>(); @GuardedBy("mAssociationsLock") private final Map mUniqueIdAssociations = new ArrayMap<>(); + // The map from input port (String) to the keyboard layout identifiers (comma separated string + // containing language tag and layout type) associated with the corresponding keyboard device. + // Currently only accessed by InputReader. + @GuardedBy("mAssociationsLock") + private final Map mKeyboardLayoutAssociations = new ArrayMap<>(); // Stores input ports associated with device types. For example, adding an association // {"123", "touchNavigation"} here would mean that a touch device appearing at port "123" would @@ -1901,8 +1906,7 @@ public class InputManagerService extends IInputManager.Stub if (!checkCallingPermission( android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY, "removeUniqueIdAssociation()")) { - throw new SecurityException( - "Requires ASSOCIATE_INPUT_DEVICE_TO_DISPLAY permission"); + throw new SecurityException("Requires ASSOCIATE_INPUT_DEVICE_TO_DISPLAY permission"); } Objects.requireNonNull(inputPort); @@ -1929,6 +1933,27 @@ public class InputManagerService extends IInputManager.Stub mNative.changeTypeAssociation(); } + private void addKeyboardLayoutAssociation(@NonNull String inputPort, + @NonNull String languageTag, @NonNull String layoutType) { + Objects.requireNonNull(inputPort); + Objects.requireNonNull(languageTag); + Objects.requireNonNull(layoutType); + + synchronized (mAssociationsLock) { + mKeyboardLayoutAssociations.put(inputPort, + TextUtils.formatSimple("%s,%s", languageTag, layoutType)); + } + mNative.changeKeyboardLayoutAssociation(); + } + + private void removeKeyboardLayoutAssociation(@NonNull String inputPort) { + Objects.requireNonNull(inputPort); + synchronized (mAssociationsLock) { + mKeyboardLayoutAssociations.remove(inputPort); + } + mNative.changeKeyboardLayoutAssociation(); + } + @Override // Binder call public InputSensorInfo[] getSensorList(int deviceId) { return mNative.getSensorList(deviceId); @@ -2673,6 +2698,17 @@ public class InputManagerService extends IInputManager.Stub return flatten(associations); } + // Native callback + @SuppressWarnings("unused") + @VisibleForTesting + private String[] getKeyboardLayoutAssociations() { + final Map configs = new ArrayMap<>(); + synchronized (mAssociationsLock) { + configs.putAll(mKeyboardLayoutAssociations); + } + return flatten(configs); + } + /** * Gets if an input device could dispatch to the given display". * @param deviceId The input device id. @@ -3316,6 +3352,18 @@ public class InputManagerService extends IInputManager.Stub public void unsetTypeAssociation(@NonNull String inputPort) { unsetTypeAssociationInternal(inputPort); } + + @Override + public void addKeyboardLayoutAssociation(@NonNull String inputPort, + @NonNull String languageTag, @NonNull String layoutType) { + InputManagerService.this.addKeyboardLayoutAssociation(inputPort, + languageTag, layoutType); + } + + @Override + public void removeKeyboardLayoutAssociation(@NonNull String inputPort) { + InputManagerService.this.removeKeyboardLayoutAssociation(inputPort); + } } @Override diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index 184bc0e3519d4..157c957ee8173 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -188,6 +188,8 @@ interface NativeInputManagerService { void changeTypeAssociation(); + void changeKeyboardLayoutAssociation(); + void notifyPointerDisplayIdChanged(); void setDisplayEligibilityForPointerCapture(int displayId, boolean enabled); @@ -404,6 +406,9 @@ interface NativeInputManagerService { @Override public native void changeTypeAssociation(); + @Override + public native void changeKeyboardLayoutAssociation(); + @Override public native void notifyPointerDisplayIdChanged(); diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index c36c571592799..d6cf4735e5853 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -121,6 +121,7 @@ static struct { jmethodID getInputPortAssociations; jmethodID getInputUniqueIdAssociations; jmethodID getDeviceTypeAssociations; + jmethodID getKeyboardLayoutAssociations; jmethodID getKeyRepeatTimeout; jmethodID getKeyRepeatDelay; jmethodID getHoverTapTimeout; @@ -412,8 +413,10 @@ private: void ensureSpriteControllerLocked(); sp getParentSurfaceForPointers(int displayId); static bool checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName); - std::unordered_map readMapFromInterleavedJavaArray( - jmethodID method, const char* methodName); + template + std::unordered_map readMapFromInterleavedJavaArray( + jmethodID method, const char* methodName, + std::function opOnValue = [](auto&& v) { return std::move(v); }); static inline JNIEnv* jniEnv() { return AndroidRuntime::getJNIEnv(); } }; @@ -588,12 +591,23 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon } outConfig->uniqueIdAssociations = - readMapFromInterleavedJavaArray(gServiceClassInfo.getInputUniqueIdAssociations, - "getInputUniqueIdAssociations"); + readMapFromInterleavedJavaArray(gServiceClassInfo + .getInputUniqueIdAssociations, + "getInputUniqueIdAssociations"); outConfig->deviceTypeAssociations = - readMapFromInterleavedJavaArray(gServiceClassInfo.getDeviceTypeAssociations, - "getDeviceTypeAssociations"); + readMapFromInterleavedJavaArray(gServiceClassInfo + .getDeviceTypeAssociations, + "getDeviceTypeAssociations"); + outConfig->keyboardLayoutAssociations = readMapFromInterleavedJavaArray< + KeyboardLayoutInfo>(gServiceClassInfo.getKeyboardLayoutAssociations, + "getKeyboardLayoutAssociations", [](auto&& layoutIdentifier) { + size_t commaPos = layoutIdentifier.find(','); + std::string languageTag = layoutIdentifier.substr(0, commaPos); + std::string layoutType = layoutIdentifier.substr(commaPos + 1); + return KeyboardLayoutInfo(std::move(languageTag), + std::move(layoutType)); + }); jint hoverTapTimeout = env->CallIntMethod(mServiceObj, gServiceClassInfo.getHoverTapTimeout); @@ -643,16 +657,18 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon } // release lock } -std::unordered_map NativeInputManager::readMapFromInterleavedJavaArray( - jmethodID method, const char* methodName) { +template +std::unordered_map NativeInputManager::readMapFromInterleavedJavaArray( + jmethodID method, const char* methodName, std::function opOnValue) { JNIEnv* env = jniEnv(); jobjectArray javaArray = jobjectArray(env->CallObjectMethod(mServiceObj, method)); - std::unordered_map map; + std::unordered_map map; if (!checkAndClearExceptionFromCallback(env, methodName) && javaArray) { jsize length = env->GetArrayLength(javaArray); for (jsize i = 0; i < length / 2; i++) { std::string key = getStringElementFromJavaArray(env, javaArray, 2 * i); - std::string value = getStringElementFromJavaArray(env, javaArray, 2 * i + 1); + T value = + opOnValue(std::move(getStringElementFromJavaArray(env, javaArray, 2 * i + 1))); map.insert({key, value}); } } @@ -2256,6 +2272,12 @@ static void nativeChangeTypeAssociation(JNIEnv* env, jobject nativeImplObj) { InputReaderConfiguration::CHANGE_DEVICE_TYPE); } +static void changeKeyboardLayoutAssociation(JNIEnv* env, jobject nativeImplObj) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + im->getInputManager()->getReader().requestRefreshConfiguration( + InputReaderConfiguration::CHANGE_KEYBOARD_LAYOUT_ASSOCIATION); +} + static void nativeSetMotionClassifierEnabled(JNIEnv* env, jobject nativeImplObj, jboolean enabled) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); @@ -2445,6 +2467,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"notifyPortAssociationsChanged", "()V", (void*)nativeNotifyPortAssociationsChanged}, {"changeUniqueIdAssociation", "()V", (void*)nativeChangeUniqueIdAssociation}, {"changeTypeAssociation", "()V", (void*)nativeChangeTypeAssociation}, + {"changeKeyboardLayoutAssociation", "()V", (void*)changeKeyboardLayoutAssociation}, {"setDisplayEligibilityForPointerCapture", "(IZ)V", (void*)nativeSetDisplayEligibilityForPointerCapture}, {"setMotionClassifierEnabled", "(Z)V", (void*)nativeSetMotionClassifierEnabled}, @@ -2569,6 +2592,9 @@ int register_android_server_InputManager(JNIEnv* env) { GET_METHOD_ID(gServiceClassInfo.getDeviceTypeAssociations, clazz, "getDeviceTypeAssociations", "()[Ljava/lang/String;"); + GET_METHOD_ID(gServiceClassInfo.getKeyboardLayoutAssociations, clazz, + "getKeyboardLayoutAssociations", "()[Ljava/lang/String;"); + GET_METHOD_ID(gServiceClassInfo.getKeyRepeatTimeout, clazz, "getKeyRepeatTimeout", "()I"); diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java index 9c7c574f8e313..c1ee88c200960 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java @@ -54,6 +54,8 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) public class InputControllerTest { + private static final String LANGUAGE_TAG = "en-US"; + private static final String LAYOUT_TYPE = "qwerty"; @Mock private InputManagerInternal mInputManagerInternalMock; @@ -100,7 +102,7 @@ public class InputControllerTest { final IBinder device2Token = new Binder("device2"); mInputController.createKeyboard("keyboard", /*vendorId= */2, /*productId= */ 2, - device2Token, 2); + device2Token, 2, LANGUAGE_TAG, LAYOUT_TYPE); int device2Id = mInputController.getInputDeviceId(device2Token); assertWithMessage("Different devices should have different id").that( @@ -178,4 +180,17 @@ public class InputControllerTest { verify(mInputManagerInternalMock).unsetTypeAssociation( startsWith("virtualNavigationTouchpad:")); } + + @Test + public void createKeyboard_addAndRemoveKeyboardLayoutAssociation() { + final IBinder deviceToken = new Binder("device"); + + mInputController.createKeyboard("keyboard", /*vendorId= */2, /*productId= */ 2, deviceToken, + 2, LANGUAGE_TAG, LAYOUT_TYPE); + verify(mInputManagerInternalMock).addKeyboardLayoutAssociation(anyString(), + eq(LANGUAGE_TAG), eq(LAYOUT_TYPE)); + + mInputController.unregisterInputDevice(deviceToken); + verify(mInputManagerInternalMock).removeKeyboardLayoutAssociation(anyString()); + } } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 31e53d56f5202..99c8302dac732 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -159,6 +159,8 @@ public class VirtualDeviceManagerServiceTest { .setProductId(PRODUCT_ID) .setInputDeviceName(DEVICE_NAME) .setAssociatedDisplayId(DISPLAY_ID) + .setLanguageTag(VirtualKeyboardConfig.DEFAULT_LANGUAGE_TAG) + .setLayoutType(VirtualKeyboardConfig.DEFAULT_LAYOUT_TYPE) .build(); private static final VirtualMouseConfig MOUSE_CONFIG = new VirtualMouseConfig.Builder() diff --git a/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt b/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt index 3326f80f5f91d..677144c144a76 100644 --- a/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt @@ -27,8 +27,6 @@ import android.view.Display import android.view.PointerIcon import androidx.test.InstrumentationRegistry import com.google.common.truth.Truth.assertThat -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before @@ -38,6 +36,7 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock +import org.mockito.Mockito.`when` import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.doAnswer import org.mockito.Mockito.never @@ -45,8 +44,9 @@ import org.mockito.Mockito.spy import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions -import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit /** * Tests for {@link InputManagerService}. @@ -311,6 +311,18 @@ class InputManagerServiceTests { assertTrue(service.getDeviceTypeAssociations().isEmpty()) } + @Test + fun testAddAndRemoveVirtualmKeyboardLayoutAssociation() { + val inputPort = "input port" + val languageTag = "language" + val layoutType = "layoutType" + localService.addKeyboardLayoutAssociation(inputPort, languageTag, layoutType) + verify(native).changeKeyboardLayoutAssociation() + + localService.removeKeyboardLayoutAssociation(inputPort) + verify(native, times(2)).changeKeyboardLayoutAssociation() + } + private fun setVirtualMousePointerDisplayIdAndVerify(overrideDisplayId: Int) { val thread = Thread { localService.setVirtualMousePointerDisplayId(overrideDisplayId) } thread.start() diff --git a/tests/Input/src/com/android/test/input/InputDeviceTest.java b/tests/Input/src/com/android/test/input/InputDeviceTest.java index 8aaf18af06644..4da530442c49f 100644 --- a/tests/Input/src/com/android/test/input/InputDeviceTest.java +++ b/tests/Input/src/com/android/test/input/InputDeviceTest.java @@ -56,6 +56,8 @@ public class InputDeviceTest { assertEquals(device.getSources(), outDevice.getSources()); assertEquals(device.getKeyboardType(), outDevice.getKeyboardType()); assertEquals(device.getCountryCode(), outDevice.getCountryCode()); + assertEquals(device.getKeyboardLanguageTag(), outDevice.getKeyboardLanguageTag()); + assertEquals(device.getKeyboardLayoutType(), outDevice.getKeyboardLayoutType()); assertEquals(device.getMotionRanges().size(), outDevice.getMotionRanges().size()); KeyCharacterMap keyCharacterMap = device.getKeyCharacterMap(); @@ -87,6 +89,8 @@ public class InputDeviceTest { .setHasSensor(true) .setHasBattery(true) .setCountryCode(InputDeviceCountryCode.INTERNATIONAL) + .setKeyboardLanguageTag("en-US") + .setKeyboardLayoutType("qwerty") .setSupportsUsi(true) .build();