Implement new Layout manager APIs

DD: go/pk_layout_selection

Test: atest FrameworksServicesTests:KeyboardLayoutManagerTests
Bug: 259530132
Change-Id: Ie7438d33a6eb8e95d3d8c5e5c55f9e7b1be7dcf4
This commit is contained in:
Vaibhav Devmurari
2022-12-22 16:13:36 +00:00
parent 170553978f
commit dd885d9ebe
7 changed files with 1772 additions and 66 deletions

View File

@@ -23,6 +23,7 @@ import android.os.Parcelable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* Describes a keyboard layout.
@@ -30,6 +31,37 @@ import java.util.Map;
* @hide
*/
public final class KeyboardLayout implements Parcelable, Comparable<KeyboardLayout> {
/** Undefined keyboard layout */
public static final String LAYOUT_TYPE_UNDEFINED = "undefined";
/** Qwerty-based keyboard layout */
public static final String LAYOUT_TYPE_QWERTY = "qwerty";
/** Qwertz-based keyboard layout */
public static final String LAYOUT_TYPE_QWERTZ = "qwertz";
/** Azerty-based keyboard layout */
public static final String LAYOUT_TYPE_AZERTY = "azerty";
/** Dvorak keyboard layout */
public static final String LAYOUT_TYPE_DVORAK = "dvorak";
/** Colemak keyboard layout */
public static final String LAYOUT_TYPE_COLEMAK = "colemak";
/** Workman keyboard layout */
public static final String LAYOUT_TYPE_WORKMAN = "workman";
/** Turkish-F keyboard layout */
public static final String LAYOUT_TYPE_TURKISH_F = "turkish_f";
/** Turkish-Q keyboard layout */
public static final String LAYOUT_TYPE_TURKISH_Q = "turkish_q";
/** Keyboard layout that has been enhanced with a large number of extra characters */
public static final String LAYOUT_TYPE_EXTENDED = "extended";
private final String mDescriptor;
private final String mLabel;
private final String mCollection;
@@ -42,31 +74,24 @@ public final class KeyboardLayout implements Parcelable, Comparable<KeyboardLayo
/** Currently supported Layout types in the KCM files */
private enum LayoutType {
UNDEFINED(0, "undefined"),
QWERTY(1, "qwerty"),
QWERTZ(2, "qwertz"),
AZERTY(3, "azerty"),
DVORAK(4, "dvorak"),
COLEMAK(5, "colemak"),
WORKMAN(6, "workman"),
TURKISH_F(7, "turkish_f"),
TURKISH_Q(8, "turkish_q"),
EXTENDED(9, "extended");
UNDEFINED(0, LAYOUT_TYPE_UNDEFINED),
QWERTY(1, LAYOUT_TYPE_QWERTY),
QWERTZ(2, LAYOUT_TYPE_QWERTZ),
AZERTY(3, LAYOUT_TYPE_AZERTY),
DVORAK(4, LAYOUT_TYPE_DVORAK),
COLEMAK(5, LAYOUT_TYPE_COLEMAK),
WORKMAN(6, LAYOUT_TYPE_WORKMAN),
TURKISH_F(7, LAYOUT_TYPE_TURKISH_F),
TURKISH_Q(8, LAYOUT_TYPE_TURKISH_Q),
EXTENDED(9, LAYOUT_TYPE_EXTENDED);
private final int mValue;
private final String mName;
private static final Map<Integer, LayoutType> VALUE_TO_ENUM_MAP = new HashMap<>();
static {
VALUE_TO_ENUM_MAP.put(UNDEFINED.mValue, UNDEFINED);
VALUE_TO_ENUM_MAP.put(QWERTY.mValue, QWERTY);
VALUE_TO_ENUM_MAP.put(QWERTZ.mValue, QWERTZ);
VALUE_TO_ENUM_MAP.put(AZERTY.mValue, AZERTY);
VALUE_TO_ENUM_MAP.put(DVORAK.mValue, DVORAK);
VALUE_TO_ENUM_MAP.put(COLEMAK.mValue, COLEMAK);
VALUE_TO_ENUM_MAP.put(WORKMAN.mValue, WORKMAN);
VALUE_TO_ENUM_MAP.put(TURKISH_F.mValue, TURKISH_F);
VALUE_TO_ENUM_MAP.put(TURKISH_Q.mValue, TURKISH_Q);
VALUE_TO_ENUM_MAP.put(EXTENDED.mValue, EXTENDED);
for (LayoutType type : LayoutType.values()) {
VALUE_TO_ENUM_MAP.put(type.mValue, type);
}
}
private static LayoutType of(int value) {
@@ -206,6 +231,9 @@ public final class KeyboardLayout implements Parcelable, Comparable<KeyboardLayo
// Note that these arguments are intentionally flipped since you want higher priority
// keyboards to be listed before lower priority keyboards.
int result = Integer.compare(another.mPriority, mPriority);
if (result == 0) {
result = Integer.compare(mLayoutType.mValue, another.mLayoutType.mValue);
}
if (result == 0) {
result = mLabel.compareToIgnoreCase(another.mLabel);
}
@@ -226,4 +254,21 @@ public final class KeyboardLayout implements Parcelable, Comparable<KeyboardLayo
+ ", vendorId: " + mVendorId
+ ", productId: " + mProductId;
}
/**
* Check if the provided layout type is supported/valid.
*
* @param layoutName name of layout type
* @return {@code true} if the provided layout type is supported/valid.
*/
public static boolean isLayoutTypeValid(@NonNull String layoutName) {
Objects.requireNonNull(layoutName, "Provided layout name should not be null");
for (LayoutType layoutType : LayoutType.values()) {
if (layoutName.equals(layoutType.getName())) {
return true;
}
}
// Layout doesn't match any supported layout types
return false;
}
}

View File

@@ -3346,10 +3346,7 @@ public class InputManagerService extends IInputManager.Stub
public void onInputMethodSubtypeChangedForKeyboardLayoutMapping(@UserIdInt int userId,
@Nullable InputMethodSubtypeHandle subtypeHandle,
@Nullable InputMethodSubtype subtype) {
if (DEBUG) {
Slog.i(TAG, "InputMethodSubtype changed: userId=" + userId
+ " subtypeHandle=" + subtypeHandle);
}
mKeyboardLayoutManager.onInputMethodSubtypeChanged(userId, subtypeHandle, subtype);
}
@Override

View File

@@ -37,6 +37,8 @@ import android.content.res.XmlResourceParser;
import android.hardware.input.InputDeviceIdentifier;
import android.hardware.input.InputManager;
import android.hardware.input.KeyboardLayout;
import android.icu.lang.UScript;
import android.icu.util.ULocale;
import android.os.Bundle;
import android.os.Handler;
import android.os.LocaleList;
@@ -45,6 +47,8 @@ import android.os.Message;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.FeatureFlagUtils;
import android.util.Log;
import android.util.Slog;
import android.view.InputDevice;
@@ -54,6 +58,7 @@ import android.widget.Toast;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.inputmethod.InputMethodSubtypeHandle;
import com.android.internal.messages.nano.SystemMessageProto;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.util.XmlUtils;
@@ -63,10 +68,12 @@ import libcore.io.Streams;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
@@ -94,10 +101,18 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
@GuardedBy("mDataStore")
private final PersistentDataStore mDataStore;
private final Handler mHandler;
private final List<InputDevice> mKeyboardsWithMissingLayouts = new ArrayList<>();
private boolean mKeyboardLayoutNotificationShown = false;
private Toast mSwitchedKeyboardLayoutToast;
// This cache stores "best-matched" layouts so that we don't need to run the matching
// algorithm repeatedly.
@GuardedBy("mKeyboardLayoutCache")
private final Map<String, String> mKeyboardLayoutCache = new ArrayMap<>();
@Nullable
private ImeInfo mCurrentImeInfo;
KeyboardLayoutManager(Context context, NativeInputManagerService nativeService,
PersistentDataStore dataStore, Looper looper) {
mContext = context;
@@ -139,8 +154,10 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
@Override
public void onInputDeviceRemoved(int deviceId) {
mKeyboardsWithMissingLayouts.removeIf(device -> device.getId() == deviceId);
maybeUpdateNotification();
if (!useNewSettingsUi()) {
mKeyboardsWithMissingLayouts.removeIf(device -> device.getId() == deviceId);
maybeUpdateNotification();
}
}
@Override
@@ -149,18 +166,21 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
return;
}
synchronized (mDataStore) {
String layout = getCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier());
if (layout == null) {
layout = getDefaultKeyboardLayout(inputDevice);
if (layout != null) {
setCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier(), layout);
} else {
mKeyboardsWithMissingLayouts.add(inputDevice);
if (!useNewSettingsUi()) {
synchronized (mDataStore) {
String layout = getCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier());
if (layout == null) {
layout = getDefaultKeyboardLayout(inputDevice);
if (layout != null) {
setCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier(), layout);
} else {
mKeyboardsWithMissingLayouts.add(inputDevice);
}
}
maybeUpdateNotification();
}
maybeUpdateNotification();
}
// TODO(b/259530132): Show notification for new Settings UI
}
private String getDefaultKeyboardLayout(final InputDevice inputDevice) {
@@ -244,6 +264,12 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
}
}
synchronized (mKeyboardLayoutCache) {
// Invalidate the cache: With packages being installed/removed, existing cache of
// auto-selected layout might not be the best layouts anymore.
mKeyboardLayoutCache.clear();
}
// Reload keyboard layouts.
reloadKeyboardLayouts();
}
@@ -256,6 +282,9 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
public KeyboardLayout[] getKeyboardLayoutsForInputDevice(
final InputDeviceIdentifier identifier) {
if (useNewSettingsUi()) {
return new KeyboardLayout[0];
}
final String[] enabledLayoutDescriptors =
getEnabledKeyboardLayoutsForInputDevice(identifier);
final ArrayList<KeyboardLayout> enabledLayouts =
@@ -296,6 +325,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
KeyboardLayout[]::new);
}
@Nullable
public KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor) {
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
@@ -434,21 +464,25 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
return LocaleList.forLanguageTags(languageTags.replace('|', ','));
}
/**
* Builds a layout descriptor for the vendor/product. This returns the
* descriptor for ids that aren't useful (such as the default 0, 0).
*/
private String getLayoutDescriptor(InputDeviceIdentifier identifier) {
private static String getLayoutDescriptor(@NonNull InputDeviceIdentifier identifier) {
Objects.requireNonNull(identifier, "identifier must not be null");
Objects.requireNonNull(identifier.getDescriptor(), "descriptor must not be null");
if (identifier.getVendorId() == 0 && identifier.getProductId() == 0) {
return identifier.getDescriptor();
}
// If vendor id and product id is available, use it as keys. This allows us to have the
// same setup for all keyboards with same product and vendor id. i.e. User can swap 2
// identical keyboards and still get the same setup.
return "vendor:" + identifier.getVendorId() + ",product:" + identifier.getProductId();
}
@Nullable
public String getCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier) {
if (useNewSettingsUi()) {
Slog.e(TAG, "getCurrentKeyboardLayoutForInputDevice API not supported");
return null;
}
String key = getLayoutDescriptor(identifier);
synchronized (mDataStore) {
String layout;
@@ -468,9 +502,13 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
public void setCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
if (useNewSettingsUi()) {
Slog.e(TAG, "setCurrentKeyboardLayoutForInputDevice API not supported");
return;
}
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
String key = getLayoutDescriptor(identifier);
synchronized (mDataStore) {
try {
@@ -489,6 +527,10 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
}
public String[] getEnabledKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) {
if (useNewSettingsUi()) {
Slog.e(TAG, "getEnabledKeyboardLayoutsForInputDevice API not supported");
return new String[0];
}
String key = getLayoutDescriptor(identifier);
synchronized (mDataStore) {
String[] layouts = mDataStore.getKeyboardLayouts(key);
@@ -502,6 +544,10 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
public void addKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
if (useNewSettingsUi()) {
Slog.e(TAG, "addKeyboardLayoutForInputDevice API not supported");
return;
}
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
@@ -525,6 +571,10 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
public void removeKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
if (useNewSettingsUi()) {
Slog.e(TAG, "removeKeyboardLayoutForInputDevice API not supported");
return;
}
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
@@ -551,31 +601,11 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
}
}
public String getKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
@UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
@Nullable InputMethodSubtype imeSubtype) {
// TODO(b/259530132): Implement the new keyboard layout API: Returning non-IME specific
// layout for now.
return getCurrentKeyboardLayoutForInputDevice(identifier);
}
public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
@UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
@Nullable InputMethodSubtype imeSubtype, String keyboardLayoutDescriptor) {
// TODO(b/259530132): Implement the new keyboard layout API: setting non-IME specific
// layout for now.
setCurrentKeyboardLayoutForInputDevice(identifier, keyboardLayoutDescriptor);
}
public KeyboardLayout[] getKeyboardLayoutListForInputDevice(InputDeviceIdentifier identifier,
@UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
@Nullable InputMethodSubtype imeSubtype) {
// TODO(b/259530132): Implement the new keyboard layout API: Returning list of all
// layouts for now.
return getKeyboardLayouts();
}
public void switchKeyboardLayout(int deviceId, int direction) {
if (useNewSettingsUi()) {
Slog.e(TAG, "switchKeyboardLayout API not supported");
return;
}
mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, deviceId, direction).sendToTarget();
}
@@ -616,8 +646,21 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
}
}
@Nullable
public String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) {
String keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier);
String keyboardLayoutDescriptor;
if (useNewSettingsUi()) {
if (mCurrentImeInfo == null) {
// Haven't received onInputMethodSubtypeChanged() callback from IMMS. Will reload
// keyboard layouts once we receive the callback.
return null;
}
keyboardLayoutDescriptor = getKeyboardLayoutForInputDeviceInternal(identifier,
mCurrentImeInfo);
} else {
keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier);
}
if (keyboardLayoutDescriptor == null) {
return null;
}
@@ -640,6 +683,287 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
return result;
}
@Nullable
public String getKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
@UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
@Nullable InputMethodSubtype imeSubtype) {
if (!useNewSettingsUi()) {
Slog.e(TAG, "getKeyboardLayoutForInputDevice() API not supported");
return null;
}
InputMethodSubtypeHandle subtypeHandle = InputMethodSubtypeHandle.of(imeInfo, imeSubtype);
String layout = getKeyboardLayoutForInputDeviceInternal(identifier,
new ImeInfo(userId, subtypeHandle, imeSubtype));
if (DEBUG) {
Slog.d(TAG, "getKeyboardLayoutForInputDevice() " + identifier.toString() + ", userId : "
+ userId + ", subtypeHandle = " + subtypeHandle + " -> " + layout);
}
return layout;
}
public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
@UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
@Nullable InputMethodSubtype imeSubtype,
String keyboardLayoutDescriptor) {
if (!useNewSettingsUi()) {
Slog.e(TAG, "setKeyboardLayoutForInputDevice() API not supported");
return;
}
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
String key = createLayoutKey(identifier, userId,
InputMethodSubtypeHandle.of(imeInfo, imeSubtype));
synchronized (mDataStore) {
try {
// Key for storing into data store = <device descriptor>,<userId>,<subtypeHandle>
if (mDataStore.setKeyboardLayout(getLayoutDescriptor(identifier), key,
keyboardLayoutDescriptor)) {
if (DEBUG) {
Slog.d(TAG, "setKeyboardLayoutForInputDevice() " + identifier
+ " key: " + key
+ " keyboardLayoutDescriptor: " + keyboardLayoutDescriptor);
}
mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
}
} finally {
mDataStore.saveIfNeeded();
}
}
}
public KeyboardLayout[] getKeyboardLayoutListForInputDevice(InputDeviceIdentifier identifier,
@UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
@Nullable InputMethodSubtype imeSubtype) {
if (!useNewSettingsUi()) {
Slog.e(TAG, "getKeyboardLayoutListForInputDevice() API not supported");
return new KeyboardLayout[0];
}
return getKeyboardLayoutListForInputDeviceInternal(identifier, new ImeInfo(userId,
InputMethodSubtypeHandle.of(imeInfo, imeSubtype), imeSubtype));
}
private KeyboardLayout[] getKeyboardLayoutListForInputDeviceInternal(
InputDeviceIdentifier identifier, ImeInfo imeInfo) {
String key = createLayoutKey(identifier, imeInfo.mUserId, imeInfo.mImeSubtypeHandle);
// Fetch user selected layout and always include it in layout list.
String userSelectedLayout;
synchronized (mDataStore) {
userSelectedLayout = mDataStore.getKeyboardLayout(getLayoutDescriptor(identifier), key);
}
final ArrayList<KeyboardLayout> potentialLayouts = new ArrayList<>();
String imeLanguageTag;
if (imeInfo.mImeSubtype == null) {
imeLanguageTag = "";
} else {
ULocale imeLocale = imeInfo.mImeSubtype.getPhysicalKeyboardHintLanguageTag();
imeLanguageTag = imeLocale != null ? imeLocale.toLanguageTag()
: imeInfo.mImeSubtype.getCanonicalizedLanguageTag();
}
visitAllKeyboardLayouts(new KeyboardLayoutVisitor() {
boolean mDeviceSpecificLayoutAvailable;
@Override
public void visitKeyboardLayout(Resources resources,
int keyboardLayoutResId, KeyboardLayout layout) {
// Next find any potential layouts that aren't yet enabled for the device. For
// devices that have special layouts we assume there's a reason that the generic
// layouts don't work for them, so we don't want to return them since it's likely
// to result in a poor user experience.
if (layout.getVendorId() == identifier.getVendorId()
&& layout.getProductId() == identifier.getProductId()) {
if (!mDeviceSpecificLayoutAvailable) {
mDeviceSpecificLayoutAvailable = true;
potentialLayouts.clear();
}
potentialLayouts.add(layout);
} else if (layout.getVendorId() == -1 && layout.getProductId() == -1
&& !mDeviceSpecificLayoutAvailable && isLayoutCompatibleWithLanguageTag(
layout, imeLanguageTag)) {
potentialLayouts.add(layout);
} else if (layout.getDescriptor().equals(userSelectedLayout)) {
potentialLayouts.add(layout);
}
}
});
// Sort the Keyboard layouts. This is done first by priority then by label. So, system
// layouts will come above 3rd party layouts.
Collections.sort(potentialLayouts);
return potentialLayouts.toArray(new KeyboardLayout[0]);
}
public void onInputMethodSubtypeChanged(@UserIdInt int userId,
@Nullable InputMethodSubtypeHandle subtypeHandle,
@Nullable InputMethodSubtype subtype) {
if (!useNewSettingsUi()) {
Slog.e(TAG, "onInputMethodSubtypeChanged() API not supported");
return;
}
if (subtypeHandle == null) {
if (DEBUG) {
Slog.d(TAG, "No InputMethod is running, ignoring change");
}
return;
}
if (mCurrentImeInfo == null || !subtypeHandle.equals(mCurrentImeInfo.mImeSubtypeHandle)
|| mCurrentImeInfo.mUserId != userId) {
mCurrentImeInfo = new ImeInfo(userId, subtypeHandle, subtype);
mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
if (DEBUG) {
Slog.d(TAG, "InputMethodSubtype changed: userId=" + userId
+ " subtypeHandle=" + subtypeHandle);
}
}
}
@Nullable
private String getKeyboardLayoutForInputDeviceInternal(InputDeviceIdentifier identifier,
ImeInfo imeInfo) {
InputDevice inputDevice = getInputDevice(identifier);
if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
return null;
}
String key = createLayoutKey(identifier, imeInfo.mUserId, imeInfo.mImeSubtypeHandle);
String layout;
synchronized (mDataStore) {
layout = mDataStore.getKeyboardLayout(getLayoutDescriptor(identifier), key);
}
if (layout == null) {
synchronized (mKeyboardLayoutCache) {
// Check Auto-selected layout cache to see if layout had been previously selected
if (mKeyboardLayoutCache.containsKey(key)) {
layout = mKeyboardLayoutCache.get(key);
} else {
// NOTE: This list is already filtered based on IME Script code
KeyboardLayout[] layoutList = getKeyboardLayoutListForInputDeviceInternal(
identifier, imeInfo);
// Call auto-matching algorithm to find the best matching layout
layout = getDefaultKeyboardLayoutBasedOnImeInfo(inputDevice, imeInfo,
layoutList);
mKeyboardLayoutCache.put(key, layout);
}
}
}
return layout;
}
@Nullable
private static String getDefaultKeyboardLayoutBasedOnImeInfo(InputDevice inputDevice,
ImeInfo imeInfo, KeyboardLayout[] layoutList) {
if (imeInfo.mImeSubtypeHandle == null) {
return null;
}
Arrays.sort(layoutList);
// Check <VendorID, ProductID> matching for explicitly declared custom KCM files.
for (KeyboardLayout layout : layoutList) {
if (layout.getVendorId() == inputDevice.getVendorId()
&& layout.getProductId() == inputDevice.getProductId()) {
if (DEBUG) {
Slog.d(TAG,
"getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on "
+ "vendor and product Ids. " + inputDevice.getIdentifier()
+ " : " + layout.getDescriptor());
}
return layout.getDescriptor();
}
}
// Check layout type, language tag information from InputDevice for matching
String inputLanguageTag = inputDevice.getKeyboardLanguageTag();
if (inputLanguageTag != null) {
String layoutDesc = getMatchingLayoutForProvidedLanguageTagAndLayoutType(layoutList,
inputLanguageTag, inputDevice.getKeyboardLayoutType());
if (layoutDesc != null) {
if (DEBUG) {
Slog.d(TAG,
"getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on "
+ "HW information (Language tag and Layout type). "
+ inputDevice.getIdentifier() + " : " + layoutDesc);
}
return layoutDesc;
}
}
InputMethodSubtype subtype = imeInfo.mImeSubtype;
// Can't auto select layout based on IME if subtype or language tag is null
if (subtype == null) {
return null;
}
// Check layout type, language tag information from IME for matching
ULocale pkLocale = subtype.getPhysicalKeyboardHintLanguageTag();
String pkLanguageTag =
pkLocale != null ? pkLocale.toLanguageTag() : subtype.getCanonicalizedLanguageTag();
String layoutDesc = getMatchingLayoutForProvidedLanguageTagAndLayoutType(layoutList,
pkLanguageTag, subtype.getPhysicalKeyboardHintLayoutType());
if (DEBUG) {
Slog.d(TAG,
"getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on "
+ "IME locale matching. " + inputDevice.getIdentifier() + " : "
+ layoutDesc);
}
return layoutDesc;
}
@Nullable
private static String getMatchingLayoutForProvidedLanguageTagAndLayoutType(
KeyboardLayout[] layoutList, @NonNull String languageTag, @Nullable String layoutType) {
if (layoutType == null || !KeyboardLayout.isLayoutTypeValid(layoutType)) {
layoutType = KeyboardLayout.LAYOUT_TYPE_UNDEFINED;
}
List<KeyboardLayout> layoutsFilteredByLayoutType = new ArrayList<>();
for (KeyboardLayout layout : layoutList) {
if (layout.getLayoutType().equals(layoutType)) {
layoutsFilteredByLayoutType.add(layout);
}
}
String layoutDesc = getMatchingLayoutForProvidedLanguageTag(layoutsFilteredByLayoutType,
languageTag);
if (layoutDesc != null) {
return layoutDesc;
}
return getMatchingLayoutForProvidedLanguageTag(Arrays.asList(layoutList), languageTag);
}
@Nullable
private static String getMatchingLayoutForProvidedLanguageTag(List<KeyboardLayout> layoutList,
@NonNull String languageTag) {
Locale locale = Locale.forLanguageTag(languageTag);
String layoutMatchingLanguage = null;
String layoutMatchingLanguageAndCountry = null;
for (KeyboardLayout layout : layoutList) {
final LocaleList locales = layout.getLocales();
for (int i = 0; i < locales.size(); i++) {
final Locale l = locales.get(i);
if (l == null) {
continue;
}
if (l.getLanguage().equals(locale.getLanguage())) {
if (layoutMatchingLanguage == null) {
layoutMatchingLanguage = layout.getDescriptor();
}
if (l.getCountry().equals(locale.getCountry())) {
if (layoutMatchingLanguageAndCountry == null) {
layoutMatchingLanguageAndCountry = layout.getDescriptor();
}
if (l.getVariant().equals(locale.getVariant())) {
return layout.getDescriptor();
}
}
}
}
}
return layoutMatchingLanguageAndCountry != null
? layoutMatchingLanguageAndCountry : layoutMatchingLanguage;
}
private void reloadKeyboardLayouts() {
if (DEBUG) {
Slog.d(TAG, "Reloading keyboard layouts.");
@@ -734,11 +1058,65 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
}
}
private boolean useNewSettingsUi() {
return FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_UI);
}
@Nullable
private InputDevice getInputDevice(int deviceId) {
InputManager inputManager = mContext.getSystemService(InputManager.class);
return inputManager != null ? inputManager.getInputDevice(deviceId) : null;
}
@Nullable
private InputDevice getInputDevice(InputDeviceIdentifier identifier) {
InputManager inputManager = mContext.getSystemService(InputManager.class);
return inputManager != null ? inputManager.getInputDeviceByDescriptor(
identifier.getDescriptor()) : null;
}
private static String createLayoutKey(InputDeviceIdentifier identifier, int userId,
@NonNull InputMethodSubtypeHandle subtypeHandle) {
Objects.requireNonNull(subtypeHandle, "subtypeHandle must not be null");
return "layoutDescriptor:" + getLayoutDescriptor(identifier) + ",userId:" + userId
+ ",subtypeHandle:" + subtypeHandle.toStringHandle();
}
private static boolean isLayoutCompatibleWithLanguageTag(KeyboardLayout layout,
@NonNull String languageTag) {
final int[] scriptsFromLanguageTag = UScript.getCode(Locale.forLanguageTag(languageTag));
if (scriptsFromLanguageTag.length == 0) {
// If no scripts inferred from languageTag then allowing the layout
return true;
}
LocaleList locales = layout.getLocales();
if (locales.isEmpty()) {
// KCM file doesn't have an associated language tag. This can be from
// a 3rd party app so need to include it as a potential layout.
return true;
}
for (int i = 0; i < locales.size(); i++) {
final Locale locale = locales.get(i);
if (locale == null) {
continue;
}
int[] scripts = UScript.getCode(locale);
if (scripts != null && haveCommonValue(scripts, scriptsFromLanguageTag)) {
return true;
}
}
return false;
}
private static boolean haveCommonValue(int[] arr1, int[] arr2) {
for (int a1 : arr1) {
for (int a2 : arr2) {
if (a1 == a2) return true;
}
}
return false;
}
private static final class KeyboardLayoutDescriptor {
public String packageName;
public String receiverName;
@@ -767,6 +1145,19 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
}
}
private static class ImeInfo {
@UserIdInt int mUserId;
@NonNull InputMethodSubtypeHandle mImeSubtypeHandle;
@Nullable InputMethodSubtype mImeSubtype;
ImeInfo(@UserIdInt int userId, @NonNull InputMethodSubtypeHandle imeSubtypeHandle,
@Nullable InputMethodSubtype imeSubtype) {
mUserId = userId;
mImeSubtypeHandle = imeSubtypeHandle;
mImeSubtype = imeSubtype;
}
}
private interface KeyboardLayoutVisitor {
void visitKeyboardLayout(Resources resources,
int keyboardLayoutResId, KeyboardLayout layout);

View File

@@ -18,6 +18,7 @@ package com.android.server.input;
import android.annotation.Nullable;
import android.hardware.input.TouchCalibration;
import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.SparseIntArray;
@@ -42,6 +43,7 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.OptionalInt;
@@ -121,6 +123,7 @@ final class PersistentDataStore {
return false;
}
@Nullable
public String getCurrentKeyboardLayout(String inputDeviceDescriptor) {
InputDeviceState state = getInputDeviceState(inputDeviceDescriptor);
return state != null ? state.getCurrentKeyboardLayout() : null;
@@ -136,6 +139,22 @@ final class PersistentDataStore {
return false;
}
@Nullable
public String getKeyboardLayout(String inputDeviceDescriptor, String key) {
InputDeviceState state = getInputDeviceState(inputDeviceDescriptor);
return state != null ? state.getKeyboardLayout(key) : null;
}
public boolean setKeyboardLayout(String inputDeviceDescriptor, String key,
String keyboardLayoutDescriptor) {
InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor);
if (state.setKeyboardLayout(key, keyboardLayoutDescriptor)) {
setDirty();
return true;
}
return false;
}
public String[] getKeyboardLayouts(String inputDeviceDescriptor) {
InputDeviceState state = getInputDeviceState(inputDeviceDescriptor);
if (state == null) {
@@ -387,6 +406,8 @@ final class PersistentDataStore {
private final ArrayList<String> mKeyboardLayouts = new ArrayList<String>();
private final SparseIntArray mKeyboardBacklightBrightnessMap = new SparseIntArray();
private final Map<String, String> mKeyboardLayoutMap = new ArrayMap<>();
public TouchCalibration getTouchCalibration(int surfaceRotation) {
try {
return mTouchCalibration[surfaceRotation];
@@ -409,6 +430,15 @@ final class PersistentDataStore {
}
}
@Nullable
public String getKeyboardLayout(String key) {
return mKeyboardLayoutMap.get(key);
}
public boolean setKeyboardLayout(String key, String keyboardLayout) {
return !Objects.equals(mKeyboardLayoutMap.put(key, keyboardLayout), keyboardLayout);
}
@Nullable
public String getCurrentKeyboardLayout() {
return mCurrentKeyboardLayout;
@@ -507,6 +537,18 @@ final class PersistentDataStore {
changed = true;
}
}
List<String> removedEntries = new ArrayList<>();
for (String key : mKeyboardLayoutMap.keySet()) {
if (!availableKeyboardLayouts.contains(mKeyboardLayoutMap.get(key))) {
removedEntries.add(key);
}
}
if (!removedEntries.isEmpty()) {
for (String key : removedEntries) {
mKeyboardLayoutMap.remove(key);
}
changed = true;
}
return changed;
}
@@ -534,6 +576,18 @@ final class PersistentDataStore {
}
mCurrentKeyboardLayout = descriptor;
}
} else if (parser.getName().equals("keyed-keyboard-layout")) {
String key = parser.getAttributeValue(null, "key");
if (key == null) {
throw new XmlPullParserException(
"Missing key attribute on keyed-keyboard-layout.");
}
String layout = parser.getAttributeValue(null, "layout");
if (layout == null) {
throw new XmlPullParserException(
"Missing layout attribute on keyed-keyboard-layout.");
}
mKeyboardLayoutMap.put(key, layout);
} else if (parser.getName().equals("light-info")) {
int lightId = parser.getAttributeInt(null, "light-id");
int lightBrightness = parser.getAttributeInt(null, "light-brightness");
@@ -607,6 +661,13 @@ final class PersistentDataStore {
serializer.endTag(null, "keyboard-layout");
}
for (String key : mKeyboardLayoutMap.keySet()) {
serializer.startTag(null, "keyed-keyboard-layout");
serializer.attribute(null, "key", key);
serializer.attribute(null, "layout", mKeyboardLayoutMap.get(key));
serializer.endTag(null, "keyed-keyboard-layout");
}
for (int i = 0; i < mKeyboardBacklightBrightnessMap.size(); i++) {
serializer.startTag(null, "light-info");
serializer.attributeInt(null, "light-id", mKeyboardBacklightBrightnessMap.keyAt(i));

View File

@@ -0,0 +1,311 @@
# Copyright (C) 2023 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.
#
# English (US) keyboard layout.
# Unlike the default (generic) keyboard layout, English (US) does not contain any
# special ALT characters.
#
type OVERLAY
### ROW 1
key GRAVE {
label: '`'
base: '`'
shift: '~'
}
key 1 {
label: '1'
base: '1'
shift: '!'
}
key 2 {
label: '2'
base: '2'
shift: '@'
}
key 3 {
label: '3'
base: '3'
shift: '#'
}
key 4 {
label: '4'
base: '4'
shift: '$'
}
key 5 {
label: '5'
base: '5'
shift: '%'
}
key 6 {
label: '6'
base: '6'
shift: '^'
}
key 7 {
label: '7'
base: '7'
shift: '&'
}
key 8 {
label: '8'
base: '8'
shift: '*'
}
key 9 {
label: '9'
base: '9'
shift: '('
}
key 0 {
label: '0'
base: '0'
shift: ')'
}
key MINUS {
label: '-'
base: '-'
shift: '_'
}
key EQUALS {
label: '='
base: '='
shift: '+'
}
### ROW 2
key Q {
label: 'Q'
base: 'q'
shift, capslock: 'Q'
}
key W {
label: 'W'
base: 'w'
shift, capslock: 'W'
}
key E {
label: 'E'
base: 'e'
shift, capslock: 'E'
}
key R {
label: 'R'
base: 'r'
shift, capslock: 'R'
}
key T {
label: 'T'
base: 't'
shift, capslock: 'T'
}
key Y {
label: 'Y'
base: 'y'
shift, capslock: 'Y'
}
key U {
label: 'U'
base: 'u'
shift, capslock: 'U'
}
key I {
label: 'I'
base: 'i'
shift, capslock: 'I'
}
key O {
label: 'O'
base: 'o'
shift, capslock: 'O'
}
key P {
label: 'P'
base: 'p'
shift, capslock: 'P'
}
key LEFT_BRACKET {
label: '['
base: '['
shift: '{'
}
key RIGHT_BRACKET {
label: ']'
base: ']'
shift: '}'
}
key BACKSLASH {
label: '\\'
base: '\\'
shift: '|'
}
### ROW 3
key A {
label: 'A'
base: 'a'
shift, capslock: 'A'
}
key S {
label: 'S'
base: 's'
shift, capslock: 'S'
}
key D {
label: 'D'
base: 'd'
shift, capslock: 'D'
}
key F {
label: 'F'
base: 'f'
shift, capslock: 'F'
}
key G {
label: 'G'
base: 'g'
shift, capslock: 'G'
}
key H {
label: 'H'
base: 'h'
shift, capslock: 'H'
}
key J {
label: 'J'
base: 'j'
shift, capslock: 'J'
}
key K {
label: 'K'
base: 'k'
shift, capslock: 'K'
}
key L {
label: 'L'
base: 'l'
shift, capslock: 'L'
}
key SEMICOLON {
label: ';'
base: ';'
shift: ':'
}
key APOSTROPHE {
label: '\''
base: '\''
shift: '"'
}
### ROW 4
key Z {
label: 'Z'
base: 'z'
shift, capslock: 'Z'
}
key X {
label: 'X'
base: 'x'
shift, capslock: 'X'
}
key C {
label: 'C'
base: 'c'
shift, capslock: 'C'
}
key V {
label: 'V'
base: 'v'
shift, capslock: 'V'
}
key B {
label: 'B'
base: 'b'
shift, capslock: 'B'
}
key N {
label: 'N'
base: 'n'
shift, capslock: 'N'
}
key M {
label: 'M'
base: 'm'
shift, capslock: 'M'
}
key COMMA {
label: ','
base: ','
shift: '<'
}
key PERIOD {
label: '.'
base: '.'
shift: '>'
}
key SLASH {
label: '/'
base: '/'
shift: '?'
}

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 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.
-->
<keyboard-layouts xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<keyboard-layout
android:name="keyboard_layout_english_uk"
android:label="English (UK)"
android:keyboardLayout="@raw/dummy_keyboard_layout"
android:keyboardLocale="en-Latn-GB"
android:keyboardLayoutType="qwerty" />
<keyboard-layout
android:name="keyboard_layout_english_us"
android:label="English (US)"
android:keyboardLayout="@raw/dummy_keyboard_layout"
android:keyboardLocale="en-Latn,en-Latn-US"
android:keyboardLayoutType="qwerty" />
<keyboard-layout
android:name="keyboard_layout_english_us_intl"
android:label="English (International)"
android:keyboardLayout="@raw/dummy_keyboard_layout"
android:keyboardLocale="en-Latn-US"
android:keyboardLayoutType="extended" />
<keyboard-layout
android:name="keyboard_layout_english_us_dvorak"
android:label="English (Dvorak)"
android:keyboardLayout="@raw/dummy_keyboard_layout"
android:keyboardLocale="en-Latn-US"
android:keyboardLayoutType="dvorak" />
<keyboard-layout
android:name="keyboard_layout_german"
android:label="German"
android:keyboardLayout="@raw/dummy_keyboard_layout"
android:keyboardLocale="de-Latn"
android:keyboardLayoutType="qwertz" />
<keyboard-layout
android:name="keyboard_layout_french"
android:label="French"
android:keyboardLayout="@raw/dummy_keyboard_layout"
android:keyboardLocale="fr-Latn-FR"
android:keyboardLayoutType="azerty" />
<keyboard-layout
android:name="keyboard_layout_russian_qwerty"
android:label="Russian"
android:keyboardLayout="@raw/dummy_keyboard_layout"
android:keyboardLocale="ru-Cyrl"
android:keyboardLayoutType="qwerty" />
<keyboard-layout
android:name="keyboard_layout_russian"
android:label="Russian"
android:keyboardLayout="@raw/dummy_keyboard_layout"
android:keyboardLocale="ru-Cyrl" />
<keyboard-layout
android:name="keyboard_layout_vendorId:1,productId:1"
android:label="vendorId:1,productId:1"
android:keyboardLayout="@raw/dummy_keyboard_layout"
androidprv:vendorId="1"
androidprv:productId="1" />
</keyboard-layouts>

View File

@@ -0,0 +1,820 @@
/*
* Copyright (C) 2023 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.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.content.pm.ServiceInfo
import android.hardware.input.IInputManager
import android.hardware.input.InputManager
import android.hardware.input.KeyboardLayout
import android.icu.lang.UScript
import android.icu.util.ULocale
import android.os.Bundle
import android.os.test.TestLooper
import android.platform.test.annotations.Presubmit
import android.provider.Settings
import android.view.InputDevice
import android.view.inputmethod.InputMethodInfo
import android.view.inputmethod.InputMethodSubtype
import androidx.test.core.R
import androidx.test.core.app.ApplicationProvider
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assert.assertThrows
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
import java.util.Locale
private fun createKeyboard(
deviceId: Int,
vendorId: Int,
productId: Int,
languageTag: String,
layoutType: String
): InputDevice =
InputDevice.Builder()
.setId(deviceId)
.setName("Device $deviceId")
.setDescriptor("descriptor $deviceId")
.setSources(InputDevice.SOURCE_KEYBOARD)
.setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
.setExternal(true)
.setVendorId(vendorId)
.setProductId(productId)
.setKeyboardLanguageTag(languageTag)
.setKeyboardLayoutType(layoutType)
.build()
/**
* Tests for {@link Default UI} and {@link New UI}.
*
* Build/Install/Run:
* atest FrameworksServicesTests:KeyboardLayoutManagerTests
*/
@Presubmit
class KeyboardLayoutManagerTests {
companion object {
const val DEVICE_ID = 1
const val VENDOR_SPECIFIC_DEVICE_ID = 2
const val ENGLISH_DVORAK_DEVICE_ID = 3
const val USER_ID = 4
const val IME_ID = "ime_id"
const val PACKAGE_NAME = "KeyboardLayoutManagerTests"
const val RECEIVER_NAME = "DummyReceiver"
private const val ENGLISH_US_LAYOUT_NAME = "keyboard_layout_english_us"
private const val ENGLISH_UK_LAYOUT_NAME = "keyboard_layout_english_uk"
private const val VENDOR_SPECIFIC_LAYOUT_NAME = "keyboard_layout_vendorId:1,productId:1"
}
private val ENGLISH_US_LAYOUT_DESCRIPTOR = createLayoutDescriptor(ENGLISH_US_LAYOUT_NAME)
private val ENGLISH_UK_LAYOUT_DESCRIPTOR = createLayoutDescriptor(ENGLISH_UK_LAYOUT_NAME)
private val VENDOR_SPECIFIC_LAYOUT_DESCRIPTOR =
createLayoutDescriptor(VENDOR_SPECIFIC_LAYOUT_NAME)
@get:Rule
val rule = MockitoJUnit.rule()!!
@Mock
private lateinit var iInputManager: IInputManager
@Mock
private lateinit var native: NativeInputManagerService
@Mock
private lateinit var packageManager: PackageManager
private lateinit var keyboardLayoutManager: KeyboardLayoutManager
private lateinit var imeInfo: InputMethodInfo
private var nextImeSubtypeId = 0
private lateinit var context: Context
private lateinit var dataStore: PersistentDataStore
private lateinit var testLooper: TestLooper
// Devices
private lateinit var keyboardDevice: InputDevice
private lateinit var vendorSpecificKeyboardDevice: InputDevice
private lateinit var englishDvorakKeyboardDevice: InputDevice
@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()
keyboardLayoutManager = KeyboardLayoutManager(context, native, dataStore, testLooper.looper)
setupInputDevices()
setupBroadcastReceiver()
setupIme()
}
private fun setupInputDevices() {
val inputManager = InputManager.resetInstance(iInputManager)
Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE)))
.thenReturn(inputManager)
keyboardDevice = createKeyboard(DEVICE_ID, 0, 0, "", "")
vendorSpecificKeyboardDevice = createKeyboard(VENDOR_SPECIFIC_DEVICE_ID, 1, 1, "", "")
englishDvorakKeyboardDevice =
createKeyboard(ENGLISH_DVORAK_DEVICE_ID, 0, 0, "en", "dvorak")
Mockito.`when`(iInputManager.inputDeviceIds)
.thenReturn(intArrayOf(DEVICE_ID, VENDOR_SPECIFIC_DEVICE_ID, ENGLISH_DVORAK_DEVICE_ID))
Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice)
Mockito.`when`(iInputManager.getInputDevice(VENDOR_SPECIFIC_DEVICE_ID))
.thenReturn(vendorSpecificKeyboardDevice)
Mockito.`when`(iInputManager.getInputDevice(ENGLISH_DVORAK_DEVICE_ID))
.thenReturn(englishDvorakKeyboardDevice)
}
private fun setupBroadcastReceiver() {
Mockito.`when`(context.packageManager).thenReturn(packageManager)
val info = createMockReceiver()
Mockito.`when`(packageManager.queryBroadcastReceivers(Mockito.any(), Mockito.anyInt()))
.thenReturn(listOf(info))
Mockito.`when`(packageManager.getReceiverInfo(Mockito.any(), Mockito.anyInt()))
.thenReturn(info.activityInfo)
val resources = context.resources
Mockito.`when`(
packageManager.getResourcesForApplication(
Mockito.any(
ApplicationInfo::class.java
)
)
).thenReturn(resources)
}
private fun setupIme() {
imeInfo = InputMethodInfo(PACKAGE_NAME, RECEIVER_NAME, "", "", 0)
}
@Test
fun testDefaultUi_getKeyboardLayouts() {
NewSettingsApiFlag(false).use {
val keyboardLayouts = keyboardLayoutManager.keyboardLayouts
assertNotEquals(
"Default UI: Keyboard layout API should not return empty array",
0,
keyboardLayouts.size
)
assertTrue(
"Default UI: Keyboard layout API should provide English(US) layout",
hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR)
)
}
}
@Test
fun testNewUi_getKeyboardLayouts() {
NewSettingsApiFlag(true).use {
val keyboardLayouts = keyboardLayoutManager.keyboardLayouts
assertNotEquals(
"New UI: Keyboard layout API should not return empty array",
0,
keyboardLayouts.size
)
assertTrue(
"New UI: Keyboard layout API should provide English(US) layout",
hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR)
)
}
}
@Test
fun testDefaultUi_getKeyboardLayoutsForInputDevice() {
NewSettingsApiFlag(false).use {
val keyboardLayouts =
keyboardLayoutManager.getKeyboardLayoutsForInputDevice(keyboardDevice.identifier)
assertNotEquals(
"Default UI: getKeyboardLayoutsForInputDevice API should not return empty array",
0,
keyboardLayouts.size
)
assertTrue(
"Default UI: getKeyboardLayoutsForInputDevice API should provide English(US) " +
"layout",
hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR)
)
val vendorSpecificKeyboardLayouts =
keyboardLayoutManager.getKeyboardLayoutsForInputDevice(
vendorSpecificKeyboardDevice.identifier
)
assertEquals(
"Default UI: getKeyboardLayoutsForInputDevice API should return only vendor " +
"specific layout",
1,
vendorSpecificKeyboardLayouts.size
)
assertEquals(
"Default UI: getKeyboardLayoutsForInputDevice API should return vendor specific " +
"layout",
VENDOR_SPECIFIC_LAYOUT_DESCRIPTOR,
vendorSpecificKeyboardLayouts[0].descriptor
)
}
}
@Test
fun testNewUi_getKeyboardLayoutsForInputDevice() {
NewSettingsApiFlag(true).use {
val keyboardLayouts =
keyboardLayoutManager.getKeyboardLayoutsForInputDevice(keyboardDevice.identifier)
assertEquals(
"New UI: getKeyboardLayoutsForInputDevice API should always return empty array",
0,
keyboardLayouts.size
)
}
}
@Test
fun testDefaultUi_getSetCurrentKeyboardLayoutForInputDevice() {
NewSettingsApiFlag(false).use {
assertNull(
"Default UI: getCurrentKeyboardLayoutForInputDevice API should return null if " +
"nothing was set",
keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
keyboardDevice.identifier
)
)
keyboardLayoutManager.setCurrentKeyboardLayoutForInputDevice(
keyboardDevice.identifier,
ENGLISH_US_LAYOUT_DESCRIPTOR
)
val keyboardLayout =
keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
keyboardDevice.identifier
)
assertEquals(
"Default UI: getCurrentKeyboardLayoutForInputDevice API should return the set " +
"layout",
ENGLISH_US_LAYOUT_DESCRIPTOR,
keyboardLayout
)
}
}
@Test
fun testNewUi_getSetCurrentKeyboardLayoutForInputDevice() {
NewSettingsApiFlag(true).use {
keyboardLayoutManager.setCurrentKeyboardLayoutForInputDevice(
keyboardDevice.identifier,
ENGLISH_US_LAYOUT_DESCRIPTOR
)
assertNull(
"New UI: getCurrentKeyboardLayoutForInputDevice API should always return null " +
"even after setCurrentKeyboardLayoutForInputDevice",
keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
keyboardDevice.identifier
)
)
}
}
@Test
fun testDefaultUi_getEnabledKeyboardLayoutsForInputDevice() {
NewSettingsApiFlag(false).use {
keyboardLayoutManager.addKeyboardLayoutForInputDevice(
keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR
)
val keyboardLayouts =
keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice(
keyboardDevice.identifier
)
assertEquals(
"Default UI: getEnabledKeyboardLayoutsForInputDevice API should return added " +
"layout",
1,
keyboardLayouts.size
)
assertEquals(
"Default UI: getEnabledKeyboardLayoutsForInputDevice API should return " +
"English(US) layout",
ENGLISH_US_LAYOUT_DESCRIPTOR,
keyboardLayouts[0]
)
assertEquals(
"Default UI: getCurrentKeyboardLayoutForInputDevice API should return " +
"English(US) layout (Auto select the first enabled layout)",
ENGLISH_US_LAYOUT_DESCRIPTOR,
keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
keyboardDevice.identifier
)
)
keyboardLayoutManager.removeKeyboardLayoutForInputDevice(
keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR
)
assertEquals(
"Default UI: getKeyboardLayoutsForInputDevice API should return 0 layouts",
0,
keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice(
keyboardDevice.identifier
).size
)
assertNull(
"Default UI: getCurrentKeyboardLayoutForInputDevice API should return null after " +
"the enabled layout is removed",
keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
keyboardDevice.identifier
)
)
}
}
@Test
fun testNewUi_getEnabledKeyboardLayoutsForInputDevice() {
NewSettingsApiFlag(true).use {
keyboardLayoutManager.addKeyboardLayoutForInputDevice(
keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR
)
assertEquals(
"New UI: getEnabledKeyboardLayoutsForInputDevice API should return always return " +
"an empty array",
0,
keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice(
keyboardDevice.identifier
).size
)
assertNull(
"New UI: getCurrentKeyboardLayoutForInputDevice API should always return null",
keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
keyboardDevice.identifier
)
)
}
}
@Test
fun testDefaultUi_switchKeyboardLayout() {
NewSettingsApiFlag(false).use {
keyboardLayoutManager.addKeyboardLayoutForInputDevice(
keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR
)
keyboardLayoutManager.addKeyboardLayoutForInputDevice(
keyboardDevice.identifier, ENGLISH_UK_LAYOUT_DESCRIPTOR
)
assertEquals(
"Default UI: getCurrentKeyboardLayoutForInputDevice API should return " +
"English(US) layout",
ENGLISH_US_LAYOUT_DESCRIPTOR,
keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
keyboardDevice.identifier
)
)
keyboardLayoutManager.switchKeyboardLayout(DEVICE_ID, 1)
// Throws null pointer because trying to show toast using TestLooper
assertThrows(NullPointerException::class.java) { testLooper.dispatchAll() }
assertEquals("Default UI: getCurrentKeyboardLayoutForInputDevice API should return " +
"English(UK) layout",
ENGLISH_UK_LAYOUT_DESCRIPTOR,
keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
keyboardDevice.identifier
)
)
}
}
@Test
fun testNewUi_switchKeyboardLayout() {
NewSettingsApiFlag(true).use {
keyboardLayoutManager.addKeyboardLayoutForInputDevice(
keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR
)
keyboardLayoutManager.addKeyboardLayoutForInputDevice(
keyboardDevice.identifier, ENGLISH_UK_LAYOUT_DESCRIPTOR
)
keyboardLayoutManager.switchKeyboardLayout(DEVICE_ID, 1)
testLooper.dispatchAll()
assertNull("New UI: getCurrentKeyboardLayoutForInputDevice API should always return " +
"null",
keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(
keyboardDevice.identifier
)
)
}
}
@Test
fun testDefaultUi_getKeyboardLayout() {
NewSettingsApiFlag(false).use {
val keyboardLayout =
keyboardLayoutManager.getKeyboardLayout(ENGLISH_US_LAYOUT_DESCRIPTOR)
assertEquals("Default UI: getKeyboardLayout API should return correct Layout from " +
"available layouts",
ENGLISH_US_LAYOUT_DESCRIPTOR,
keyboardLayout!!.descriptor
)
}
}
@Test
fun testNewUi_getKeyboardLayout() {
NewSettingsApiFlag(true).use {
val keyboardLayout =
keyboardLayoutManager.getKeyboardLayout(ENGLISH_US_LAYOUT_DESCRIPTOR)
assertEquals("New UI: getKeyboardLayout API should return correct Layout from " +
"available layouts",
ENGLISH_US_LAYOUT_DESCRIPTOR,
keyboardLayout!!.descriptor
)
}
}
@Test
fun testDefaultUi_getSetKeyboardLayoutForInputDevice_WithImeInfo() {
NewSettingsApiFlag(false).use {
val imeSubtype = createImeSubtype()
keyboardLayoutManager.setKeyboardLayoutForInputDevice(
keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype,
ENGLISH_UK_LAYOUT_DESCRIPTOR
)
val keyboardLayout =
keyboardLayoutManager.getKeyboardLayoutForInputDevice(
keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype
)
assertNull(
"Default UI: getKeyboardLayoutForInputDevice API should always return null",
keyboardLayout
)
}
}
@Test
fun testNewUi_getSetKeyboardLayoutForInputDevice_withImeInfo() {
NewSettingsApiFlag(true).use {
val imeSubtype = createImeSubtype()
keyboardLayoutManager.setKeyboardLayoutForInputDevice(
keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype,
ENGLISH_UK_LAYOUT_DESCRIPTOR
)
assertEquals(
"New UI: getKeyboardLayoutForInputDevice API should return the set layout",
ENGLISH_UK_LAYOUT_DESCRIPTOR,
keyboardLayoutManager.getKeyboardLayoutForInputDevice(
keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype
)
)
// This should replace previously set layout
keyboardLayoutManager.setKeyboardLayoutForInputDevice(
keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype,
ENGLISH_US_LAYOUT_DESCRIPTOR
)
assertEquals(
"New UI: getKeyboardLayoutForInputDevice API should return the last set layout",
ENGLISH_US_LAYOUT_DESCRIPTOR,
keyboardLayoutManager.getKeyboardLayoutForInputDevice(
keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype
)
)
}
}
@Test
fun testDefaultUi_getKeyboardLayoutListForInputDevice() {
NewSettingsApiFlag(false).use {
val keyboardLayouts =
keyboardLayoutManager.getKeyboardLayoutListForInputDevice(
keyboardDevice.identifier, USER_ID, imeInfo,
createImeSubtype()
)
assertEquals("Default UI: getKeyboardLayoutListForInputDevice API should always " +
"return empty array",
0,
keyboardLayouts.size
)
}
}
@Test
fun testNewUi_getKeyboardLayoutListForInputDevice() {
NewSettingsApiFlag(true).use {
// Check Layouts for "hi-Latn". It should return all 'Latn' keyboard layouts
var keyboardLayouts =
keyboardLayoutManager.getKeyboardLayoutListForInputDevice(
keyboardDevice.identifier, USER_ID, imeInfo,
createImeSubtypeForLanguageTag("hi-Latn")
)
assertNotEquals(
"New UI: getKeyboardLayoutListForInputDevice API should return the list of " +
"supported layouts with matching script code",
0,
keyboardLayouts.size
)
val englishScripts = UScript.getCode(Locale.forLanguageTag("hi-Latn"))
for (kl in keyboardLayouts) {
var isCompatible = false
for (i in 0 until kl.locales.size()) {
val locale: Locale = kl.locales.get(i) ?: continue
val scripts = UScript.getCode(locale)
if (scripts != null && areScriptsCompatible(scripts, englishScripts)) {
isCompatible = true
break
}
}
assertTrue(
"New UI: getKeyboardLayoutListForInputDevice API should only return " +
"compatible layouts but found " + kl.descriptor,
isCompatible
)
}
// Check Layouts for "hi" which by default uses 'Deva' script.
keyboardLayouts =
keyboardLayoutManager.getKeyboardLayoutListForInputDevice(
keyboardDevice.identifier, USER_ID, imeInfo,
createImeSubtypeForLanguageTag("hi")
)
assertEquals("New UI: getKeyboardLayoutListForInputDevice API should return empty " +
"list if no supported layouts available",
0,
keyboardLayouts.size
)
// If user manually selected some layout, always provide it in the layout list
val imeSubtype = createImeSubtypeForLanguageTag("hi")
keyboardLayoutManager.setKeyboardLayoutForInputDevice(
keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype,
ENGLISH_US_LAYOUT_DESCRIPTOR
)
keyboardLayouts =
keyboardLayoutManager.getKeyboardLayoutListForInputDevice(
keyboardDevice.identifier, USER_ID, imeInfo,
imeSubtype
)
assertEquals("New UI: getKeyboardLayoutListForInputDevice API should return user " +
"selected layout even if the script is incompatible with IME",
1,
keyboardLayouts.size
)
}
}
@Test
fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withImeLanguageTag() {
NewSettingsApiFlag(true).use {
assertCorrectLayout(
keyboardDevice,
createImeSubtypeForLanguageTag("en-US"),
ENGLISH_US_LAYOUT_DESCRIPTOR
)
assertCorrectLayout(
keyboardDevice,
createImeSubtypeForLanguageTag("en-GB"),
ENGLISH_UK_LAYOUT_DESCRIPTOR
)
assertCorrectLayout(
keyboardDevice,
createImeSubtypeForLanguageTag("de"),
createLayoutDescriptor("keyboard_layout_german")
)
assertCorrectLayout(
keyboardDevice,
createImeSubtypeForLanguageTag("fr-FR"),
createLayoutDescriptor("keyboard_layout_french")
)
assertCorrectLayout(
keyboardDevice,
createImeSubtypeForLanguageTag("ru"),
createLayoutDescriptor("keyboard_layout_russian")
)
assertNull(
"New UI: getDefaultKeyboardLayoutForInputDevice should return null when no " +
"layout available",
keyboardLayoutManager.getKeyboardLayoutForInputDevice(
keyboardDevice.identifier, USER_ID, imeInfo,
createImeSubtypeForLanguageTag("it")
)
)
assertNull(
"New UI: getDefaultKeyboardLayoutForInputDevice should return null when no " +
"layout for script code is available",
keyboardLayoutManager.getKeyboardLayoutForInputDevice(
keyboardDevice.identifier, USER_ID, imeInfo,
createImeSubtypeForLanguageTag("en-Deva")
)
)
}
}
@Test
fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withImeLanguageTagAndLayoutType() {
NewSettingsApiFlag(true).use {
assertCorrectLayout(
keyboardDevice,
createImeSubtypeForLanguageTagAndLayoutType("en-US", "qwerty"),
ENGLISH_US_LAYOUT_DESCRIPTOR
)
assertCorrectLayout(
keyboardDevice,
createImeSubtypeForLanguageTagAndLayoutType("en-US", "dvorak"),
createLayoutDescriptor("keyboard_layout_english_us_dvorak")
)
// Try to match layout type even if country doesn't match
assertCorrectLayout(
keyboardDevice,
createImeSubtypeForLanguageTagAndLayoutType("en-GB", "dvorak"),
createLayoutDescriptor("keyboard_layout_english_us_dvorak")
)
// Choose layout based on layout type priority, if layout type is not provided by IME
// (Qwerty > Dvorak > Extended)
assertCorrectLayout(
keyboardDevice,
createImeSubtypeForLanguageTagAndLayoutType("en-US", ""),
ENGLISH_US_LAYOUT_DESCRIPTOR
)
assertCorrectLayout(
keyboardDevice,
createImeSubtypeForLanguageTagAndLayoutType("en-GB", "qwerty"),
ENGLISH_UK_LAYOUT_DESCRIPTOR
)
assertCorrectLayout(
keyboardDevice,
createImeSubtypeForLanguageTagAndLayoutType("de", "qwertz"),
createLayoutDescriptor("keyboard_layout_german")
)
// Wrong layout type should match with language if provided layout type not available
assertCorrectLayout(
keyboardDevice,
createImeSubtypeForLanguageTagAndLayoutType("de", "qwerty"),
createLayoutDescriptor("keyboard_layout_german")
)
assertCorrectLayout(
keyboardDevice,
createImeSubtypeForLanguageTagAndLayoutType("fr-FR", "azerty"),
createLayoutDescriptor("keyboard_layout_french")
)
assertCorrectLayout(
keyboardDevice,
createImeSubtypeForLanguageTagAndLayoutType("ru", "qwerty"),
createLayoutDescriptor("keyboard_layout_russian_qwerty")
)
// If layout type is empty then prioritize KCM with empty layout type
assertCorrectLayout(
keyboardDevice,
createImeSubtypeForLanguageTagAndLayoutType("ru", ""),
createLayoutDescriptor("keyboard_layout_russian")
)
assertNull("New UI: getDefaultKeyboardLayoutForInputDevice should return null when " +
"no layout for script code is available",
keyboardLayoutManager.getKeyboardLayoutForInputDevice(
keyboardDevice.identifier, USER_ID, imeInfo,
createImeSubtypeForLanguageTagAndLayoutType("en-Deva-US", "")
)
)
}
}
@Test
fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withHwLanguageTagAndLayoutType() {
NewSettingsApiFlag(true).use {
// Should return English dvorak even if IME current layout is qwerty, since HW says the
// keyboard is a Dvorak keyboard
assertCorrectLayout(
englishDvorakKeyboardDevice,
createImeSubtypeForLanguageTagAndLayoutType("en", "qwerty"),
createLayoutDescriptor("keyboard_layout_english_us_dvorak")
)
// Fallback to IME information if the HW provided layout script is incompatible with the
// provided IME subtype
assertCorrectLayout(
englishDvorakKeyboardDevice,
createImeSubtypeForLanguageTagAndLayoutType("ru", ""),
createLayoutDescriptor("keyboard_layout_russian")
)
}
}
private fun assertCorrectLayout(
device: InputDevice,
imeSubtype: InputMethodSubtype,
expectedLayout: String
) {
assertEquals(
"New UI: getDefaultKeyboardLayoutForInputDevice should return $expectedLayout",
expectedLayout,
keyboardLayoutManager.getKeyboardLayoutForInputDevice(
device.identifier, USER_ID, imeInfo, imeSubtype
)
)
}
private fun createImeSubtype(): InputMethodSubtype =
InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(nextImeSubtypeId++).build()
private fun createImeSubtypeForLanguageTag(languageTag: String): InputMethodSubtype =
InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(nextImeSubtypeId++)
.setLanguageTag(languageTag).build()
private fun createImeSubtypeForLanguageTagAndLayoutType(
languageTag: String,
layoutType: String
): InputMethodSubtype =
InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(nextImeSubtypeId++)
.setPhysicalKeyboardHint(ULocale.forLanguageTag(languageTag), layoutType).build()
private fun hasLayout(layoutList: Array<KeyboardLayout>, layoutDesc: String): Boolean {
for (kl in layoutList) {
if (kl.descriptor == layoutDesc) {
return true
}
}
return false
}
private fun createLayoutDescriptor(keyboardName: String): String =
"$PACKAGE_NAME/$RECEIVER_NAME/$keyboardName"
private fun areScriptsCompatible(scriptList1: IntArray, scriptList2: IntArray): Boolean {
for (s1 in scriptList1) {
for (s2 in scriptList2) {
if (s1 == s2) return true
}
}
return false
}
private fun createMockReceiver(): ResolveInfo {
val info = ResolveInfo()
info.activityInfo = ActivityInfo()
info.activityInfo.packageName = PACKAGE_NAME
info.activityInfo.name = RECEIVER_NAME
info.activityInfo.applicationInfo = ApplicationInfo()
info.activityInfo.metaData = Bundle()
info.activityInfo.metaData.putInt(
InputManager.META_DATA_KEYBOARD_LAYOUTS,
R.xml.keyboard_layouts
)
info.serviceInfo = ServiceInfo()
info.serviceInfo.packageName = PACKAGE_NAME
info.serviceInfo.name = RECEIVER_NAME
return info
}
private inner class NewSettingsApiFlag constructor(enabled: Boolean) : AutoCloseable {
init {
Settings.Global.putString(
context.contentResolver,
"settings_new_keyboard_ui", enabled.toString()
)
}
override fun close() {
Settings.Global.putString(
context.contentResolver,
"settings_new_keyboard_ui",
""
)
}
}
}