From aa1b2cd12dc68e80ab52e42e6f8ff082482fe0e4 Mon Sep 17 00:00:00 2001 From: Vaibhav Devmurari Date: Wed, 7 Jun 2023 14:25:49 +0000 Subject: [PATCH] Log KeyboardConfigured atom Bug: 280421650 Test: atest FrameworksServicesTests:KeyboardMetricsCollectorTests Change-Id: I0d631acb9c75f3794265999487341d77ff29fc35 --- .../hardware/input/KeyboardLayout.java | 23 +- .../server/input/KeyboardLayoutManager.java | 181 ++++++++---- .../input/KeyboardMetricsCollector.java | 276 +++++++++++++----- .../server/input/PersistentDataStore.java | 4 + .../input/KeyboardMetricsCollectorTests.kt | 179 ++++++++++++ 5 files changed, 543 insertions(+), 120 deletions(-) create mode 100644 services/tests/servicestests/src/com/android/server/input/KeyboardMetricsCollectorTests.kt diff --git a/core/java/android/hardware/input/KeyboardLayout.java b/core/java/android/hardware/input/KeyboardLayout.java index 0311da4b645fb..4403251e0488d 100644 --- a/core/java/android/hardware/input/KeyboardLayout.java +++ b/core/java/android/hardware/input/KeyboardLayout.java @@ -73,7 +73,7 @@ public final class KeyboardLayout implements Parcelable, Comparable VALUE_TO_ENUM_MAP = new HashMap<>(); + private static final Map NAME_TO_ENUM_MAP = new HashMap<>(); static { for (LayoutType type : LayoutType.values()) { VALUE_TO_ENUM_MAP.put(type.mValue, type); + NAME_TO_ENUM_MAP.put(type.mName, type); } } @@ -110,6 +112,25 @@ public final class KeyboardLayout implements Parcelable, Comparable mKeyboardLayoutCache = new ArrayMap<>(); + private final Map mKeyboardLayoutCache = new ArrayMap<>(); private final Object mImeInfoLock = new Object(); @Nullable @GuardedBy("mImeInfoLock") @@ -194,7 +200,8 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { setCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier(), layout); } } - config.setCurrentLayout(layout); + config.setCurrentLayout( + new KeyboardLayoutInfo(layout, LAYOUT_SELECTION_CRITERIA_USER)); if (layout == null) { // In old settings show notification always until user manually selects a // layout in the settings. @@ -205,18 +212,19 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { final InputDeviceIdentifier identifier = inputDevice.getIdentifier(); final String key = getLayoutDescriptor(identifier); Set selectedLayouts = new HashSet<>(); - for (ImeInfo imeInfo : getImeInfoListForLayoutMapping()) { + List imeInfoList = getImeInfoListForLayoutMapping(); + List layoutInfoList = new ArrayList<>(); + boolean hasMissingLayout = false; + for (ImeInfo imeInfo : imeInfoList) { // Check if the layout has been previously configured - String layout = getKeyboardLayoutForInputDeviceInternal(identifier, - new ImeInfo(imeInfo.mUserId, imeInfo.mImeSubtypeHandle, - imeInfo.mImeSubtype)); - if (layout == null) { - // If even one layout not configured properly, we need to ask user to configure - // the keyboard properly from the Settings. - selectedLayouts.clear(); - break; + KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal(identifier, + imeInfo); + boolean noLayoutFound = layoutInfo == null || layoutInfo.mDescriptor == null; + if (!noLayoutFound) { + selectedLayouts.add(layoutInfo.mDescriptor); } - selectedLayouts.add(layout); + layoutInfoList.add(layoutInfo); + hasMissingLayout |= noLayoutFound; } if (DEBUG) { @@ -225,24 +233,38 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { + selectedLayouts); } + // If even one layout not configured properly, we need to ask user to configure + // the keyboard properly from the Settings. + if (hasMissingLayout) { + selectedLayouts.clear(); + } + config.setConfiguredLayouts(selectedLayouts); // Update current layout: If there is a change then need to reload. synchronized (mImeInfoLock) { - String layout = getKeyboardLayoutForInputDeviceInternal( + KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal( inputDevice.getIdentifier(), mCurrentImeInfo); - if (!Objects.equals(layout, config.getCurrentLayout())) { - config.setCurrentLayout(layout); + if (!Objects.equals(layoutInfo, config.getCurrentLayout())) { + config.setCurrentLayout(layoutInfo); mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS); } } synchronized (mDataStore) { try { + boolean isFirstConfiguration = !mDataStore.hasInputDeviceEntry(key); if (mDataStore.setSelectedKeyboardLayouts(key, selectedLayouts)) { // Need to show the notification only if layout selection changed // from the previous configuration needToShowNotification = true; + + // Logging keyboard configuration data to statsd only if the + // configuration changed from the previous configuration. Currently + // only logging for New Settings UI where we are using IME to decide + // the layout information. + logKeyboardConfigurationEvent(inputDevice, imeInfoList, layoutInfoList, + isFirstConfiguration); } } finally { mDataStore.saveIfNeeded(); @@ -252,8 +274,6 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { if (needToShowNotification) { maybeUpdateNotification(); } - // TODO (b/280421650): Implement logging statements using KeyboardMetricsCollector - // for KeyboardConfigured atom } private String getDefaultKeyboardLayout(final InputDevice inputDevice) { @@ -403,7 +423,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { @AnyThread @Nullable - public KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor) { + public KeyboardLayout getKeyboardLayout(@NonNull String keyboardLayoutDescriptor) { Objects.requireNonNull(keyboardLayoutDescriptor, "keyboardLayoutDescriptor must not be null"); @@ -751,8 +771,9 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { String keyboardLayoutDescriptor; if (useNewSettingsUi()) { synchronized (mImeInfoLock) { - keyboardLayoutDescriptor = getKeyboardLayoutForInputDeviceInternal(identifier, + KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal(identifier, mCurrentImeInfo); + keyboardLayoutDescriptor = layoutInfo == null ? null : layoutInfo.mDescriptor; } } else { keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier); @@ -789,13 +810,13 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { return null; } InputMethodSubtypeHandle subtypeHandle = InputMethodSubtypeHandle.of(imeInfo, imeSubtype); - String layout = getKeyboardLayoutForInputDeviceInternal(identifier, + KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal(identifier, new ImeInfo(userId, subtypeHandle, imeSubtype)); if (DEBUG) { Slog.d(TAG, "getKeyboardLayoutForInputDevice() " + identifier.toString() + ", userId : " - + userId + ", subtypeHandle = " + subtypeHandle + " -> " + layout); + + userId + ", subtypeHandle = " + subtypeHandle + " -> " + layoutInfo); } - return layout; + return layoutInfo != null ? layoutInfo.mDescriptor : null; } @AnyThread @@ -926,11 +947,11 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { for (int i = 0; i < mConfiguredKeyboards.size(); i++) { InputDevice inputDevice = Objects.requireNonNull( getInputDevice(mConfiguredKeyboards.keyAt(i))); - String layout = getKeyboardLayoutForInputDeviceInternal(inputDevice.getIdentifier(), - mCurrentImeInfo); + KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal( + inputDevice.getIdentifier(), mCurrentImeInfo); KeyboardConfiguration config = mConfiguredKeyboards.valueAt(i); - if (!Objects.equals(layout, config.getCurrentLayout())) { - config.setCurrentLayout(layout); + if (!Objects.equals(layoutInfo, config.getCurrentLayout())) { + config.setCurrentLayout(layoutInfo); mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS); return; } @@ -939,39 +960,40 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { } @Nullable - private String getKeyboardLayoutForInputDeviceInternal(InputDeviceIdentifier identifier, - @Nullable ImeInfo imeInfo) { + private KeyboardLayoutInfo getKeyboardLayoutForInputDeviceInternal( + InputDeviceIdentifier identifier, @Nullable ImeInfo imeInfo) { InputDevice inputDevice = getInputDevice(identifier); if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) { return null; } String key = createLayoutKey(identifier, imeInfo); - 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); - } + String layout = mDataStore.getKeyboardLayout(getLayoutDescriptor(identifier), key); + if (layout != null) { + return new KeyboardLayoutInfo(layout, LAYOUT_SELECTION_CRITERIA_USER); + } + } + + synchronized (mKeyboardLayoutCache) { + // Check Auto-selected layout cache to see if layout had been previously selected + if (mKeyboardLayoutCache.containsKey(key)) { + return 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 + KeyboardLayoutInfo layoutInfo = + getDefaultKeyboardLayoutBasedOnImeInfo(inputDevice, imeInfo, layoutList); + mKeyboardLayoutCache.put(key, layoutInfo); + return layoutInfo; } } - return layout; } @Nullable - private static String getDefaultKeyboardLayoutBasedOnImeInfo(InputDevice inputDevice, - @Nullable ImeInfo imeInfo, KeyboardLayout[] layoutList) { + private static KeyboardLayoutInfo getDefaultKeyboardLayoutBasedOnImeInfo( + InputDevice inputDevice, @Nullable ImeInfo imeInfo, KeyboardLayout[] layoutList) { Arrays.sort(layoutList); // Check matching for explicitly declared custom KCM files. @@ -984,7 +1006,8 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { + "vendor and product Ids. " + inputDevice.getIdentifier() + " : " + layout.getDescriptor()); } - return layout.getDescriptor(); + return new KeyboardLayoutInfo(layout.getDescriptor(), + LAYOUT_SELECTION_CRITERIA_DEVICE); } } @@ -1001,7 +1024,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { + "HW information (Language tag and Layout type). " + inputDevice.getIdentifier() + " : " + layoutDesc); } - return layoutDesc; + return new KeyboardLayoutInfo(layoutDesc, LAYOUT_SELECTION_CRITERIA_DEVICE); } } @@ -1023,7 +1046,10 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { + "IME locale matching. " + inputDevice.getIdentifier() + " : " + layoutDesc); } - return layoutDesc; + if (layoutDesc != null) { + return new KeyboardLayoutInfo(layoutDesc, LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD); + } + return null; } @Nullable @@ -1229,6 +1255,26 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } + private void logKeyboardConfigurationEvent(@NonNull InputDevice inputDevice, + @NonNull List imeInfoList, @NonNull List layoutInfoList, + boolean isFirstConfiguration) { + if (imeInfoList.isEmpty() || layoutInfoList.isEmpty()) { + return; + } + KeyboardConfigurationEvent.Builder configurationEventBuilder = + new KeyboardConfigurationEvent.Builder(inputDevice).setIsFirstTimeConfiguration( + isFirstConfiguration); + for (int i = 0; i < imeInfoList.size(); i++) { + KeyboardLayoutInfo layoutInfo = layoutInfoList.get(i); + boolean noLayoutFound = layoutInfo == null || layoutInfo.mDescriptor == null; + configurationEventBuilder.addLayoutSelection(imeInfoList.get(i).mImeSubtype, + noLayoutFound ? null : getKeyboardLayout(layoutInfo.mDescriptor), + noLayoutFound ? LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD + : layoutInfo.mSelectionCriteria); + } + KeyboardMetricsCollector.logKeyboardConfiguredAtom(configurationEventBuilder.build()); + } + private boolean handleMessage(Message msg) { switch (msg.what) { case MSG_UPDATE_EXISTING_DEVICES: @@ -1411,7 +1457,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { // If null, it means no layout is selected for the device. @Nullable - private String mCurrentLayout; + private KeyboardLayoutInfo mCurrentLayout; private boolean hasConfiguredLayouts() { return mConfiguredLayouts != null && !mConfiguredLayouts.isEmpty(); @@ -1427,15 +1473,42 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { } @Nullable - private String getCurrentLayout() { + private KeyboardLayoutInfo getCurrentLayout() { return mCurrentLayout; } - private void setCurrentLayout(String currentLayout) { + private void setCurrentLayout(KeyboardLayoutInfo currentLayout) { mCurrentLayout = currentLayout; } } + private static class KeyboardLayoutInfo { + @Nullable + private final String mDescriptor; + @LayoutSelectionCriteria + private final int mSelectionCriteria; + + private KeyboardLayoutInfo(@Nullable String descriptor, + @LayoutSelectionCriteria int selectionCriteria) { + mDescriptor = descriptor; + mSelectionCriteria = selectionCriteria; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof KeyboardLayoutInfo) { + return Objects.equals(mDescriptor, ((KeyboardLayoutInfo) obj).mDescriptor) + && mSelectionCriteria == ((KeyboardLayoutInfo) obj).mSelectionCriteria; + } + return false; + } + + @Override + public int hashCode() { + return 31 * mSelectionCriteria + mDescriptor.hashCode(); + } + } + private interface KeyboardLayoutVisitor { void visitKeyboardLayout(Resources resources, int keyboardLayoutResId, KeyboardLayout layout); diff --git a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java index b8f57f555c0ec..19fa7a8e0acaf 100644 --- a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java +++ b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java @@ -19,16 +19,25 @@ package com.android.server.input; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.hardware.input.KeyboardLayout; +import android.icu.util.ULocale; +import android.util.Log; +import android.util.Slog; import android.util.proto.ProtoOutputStream; import android.view.InputDevice; +import android.view.inputmethod.InputMethodSubtype; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.KeyboardConfiguredProto.KeyboardLayoutConfig; import com.android.internal.os.KeyboardConfiguredProto.RepeatedKeyboardLayoutConfig; import com.android.internal.util.FrameworkStatsLog; import java.lang.annotation.Retention; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * Collect Keyboard metrics @@ -36,6 +45,30 @@ import java.util.List; public final class KeyboardMetricsCollector { private static final String TAG = "KeyboardMetricCollector"; + // To enable these logs, run: 'adb shell setprop log.tag.KeyboardMetricCollector DEBUG' + // (requires restart) + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + @Retention(SOURCE) + @IntDef(prefix = { "LAYOUT_SELECTION_CRITERIA_" }, value = { + LAYOUT_SELECTION_CRITERIA_USER, + LAYOUT_SELECTION_CRITERIA_DEVICE, + LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD + }) + public @interface LayoutSelectionCriteria {} + + /** Manual selection by user */ + public static final int LAYOUT_SELECTION_CRITERIA_USER = 0; + + /** Auto-detection based on device provided language tag and layout type */ + public static final int LAYOUT_SELECTION_CRITERIA_DEVICE = 1; + + /** Auto-detection based on IME provided language tag and layout type */ + public static final int LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD = 2; + + @VisibleForTesting + static final String DEFAULT_LAYOUT = "Default"; + /** * Log keyboard system shortcuts for the proto * {@link com.android.os.input.KeyboardSystemsEventReported} @@ -43,120 +76,233 @@ public final class KeyboardMetricsCollector { */ public static void logKeyboardSystemsEventReportedAtom(InputDevice inputDevice, int keyboardSystemEvent, int[] keyCode, int modifierState) { - int vendor_id = inputDevice.getVendorId(); - int product_id = inputDevice.getProductId(); + int vendorId = inputDevice.getVendorId(); + int productId = inputDevice.getProductId(); FrameworkStatsLog.write(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED, - vendor_id, product_id, keyboardSystemEvent, keyCode, modifierState); + vendorId, productId, keyboardSystemEvent, keyCode, modifierState); } /** * Function to log the KeyboardConfigured * {@link com.android.os.input.KeyboardConfigured} atom * - * @param inputDevice Input device - * @param keyboardLayoutConfigurations List of keyboard configurations - * @param isFirstTimeConfiguration Whether keyboard is configured for the first time + * @param event {@link KeyboardConfigurationEvent} contains information about keyboard + * configuration. Use {@link KeyboardConfigurationEvent.Builder} to create the + * configuration event to log. */ - public static void logKeyboardConfiguredAtom(InputDevice inputDevice, - List keyboardLayoutConfigurations, - boolean isFirstTimeConfiguration) { - int vendor_id = inputDevice.getVendorId(); - int product_id = inputDevice.getProductId(); - + public static void logKeyboardConfiguredAtom(KeyboardConfigurationEvent event) { // Creating proto to log nested field KeyboardLayoutConfig in atom ProtoOutputStream proto = new ProtoOutputStream(); - for (KeyboardLayoutConfiguration keyboardLayoutConfiguration : - keyboardLayoutConfigurations) { - addKeyboardLayoutConfigurationToProto(proto, keyboardLayoutConfiguration); + for (LayoutConfiguration layoutConfiguration : event.getLayoutConfigurations()) { + addKeyboardLayoutConfigurationToProto(proto, layoutConfiguration); } // Push the atom to Statsd FrameworkStatsLog.write(FrameworkStatsLog.KEYBOARD_CONFIGURED, - isFirstTimeConfiguration, vendor_id, product_id, proto.getBytes()); + event.isFirstConfiguration(), event.getVendorId(), event.getProductId(), + proto.getBytes()); + + if (DEBUG) { + Slog.d(TAG, "Logging Keyboard configuration event: " + event); + } } /** * Populate the KeyboardLayoutConfig proto which is a repeated proto * in the RepeatedKeyboardLayoutConfig proto with values from the - * {@link KeyboardLayoutConfiguration} class + * {@link LayoutConfiguration} class * The proto definitions can be found at: * "frameworks/proto_logging/stats/atoms/input/input_extension_atoms.proto" * * @param proto Representing the nested proto RepeatedKeyboardLayoutConfig - * @param keyboardLayoutConfiguration Class containing the fields for populating the + * @param layoutConfiguration Class containing the fields for populating the * KeyboardLayoutConfig proto */ private static void addKeyboardLayoutConfigurationToProto(ProtoOutputStream proto, - KeyboardLayoutConfiguration keyboardLayoutConfiguration) { + LayoutConfiguration layoutConfiguration) { // Start a new KeyboardLayoutConfig proto. long keyboardLayoutConfigToken = proto.start( RepeatedKeyboardLayoutConfig.KEYBOARD_LAYOUT_CONFIG); proto.write(KeyboardLayoutConfig.KEYBOARD_LANGUAGE_TAG, - keyboardLayoutConfiguration.getKeyboardLanguageTag()); + layoutConfiguration.keyboardLanguageTag); proto.write(KeyboardLayoutConfig.KEYBOARD_LAYOUT_TYPE, - keyboardLayoutConfiguration.getKeyboardLayoutType()); + layoutConfiguration.keyboardLayoutType); proto.write(KeyboardLayoutConfig.KEYBOARD_LAYOUT_NAME, - keyboardLayoutConfiguration.getKeyboardLayoutName()); + layoutConfiguration.keyboardLayoutName); proto.write(KeyboardLayoutConfig.LAYOUT_SELECTION_CRITERIA, - keyboardLayoutConfiguration.getLayoutSelectionCriteria()); + layoutConfiguration.layoutSelectionCriteria); proto.end(keyboardLayoutConfigToken); } /** - * Java class representing the proto KeyboardLayoutConfig defined in - * "frameworks/proto_logging/stats/atoms/input/input_extension_atoms.proto" + * Class representing the proto KeyboardLayoutConfig defined in + * "frameworks/proto_logging/stats/atoms/input/input_extension_atoms.proto * * @see com.android.os.input.KeyboardConfigured */ - public static class KeyboardLayoutConfiguration { - // KeyboardLayoutType in "frameworks/base/core/res/res/values/attrs.xml" - // contains mapping for enums to int - int mKeyboardLayoutType; - String mKeyboardLanguageTag; - KeyboardLayout mKeyboardLayout; - @LayoutSelectionCriteria int mLayoutSelectionCriteria; + public static class KeyboardConfigurationEvent { - @Retention(SOURCE) - @IntDef(prefix = { "LAYOUT_SELECTION_CRITERIA_" }, value = { - LAYOUT_SELECTION_CRITERIA_USER, - LAYOUT_SELECTION_CRITERIA_DEVICE, - LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD - }) - public @interface LayoutSelectionCriteria {} + private final InputDevice mInputDevice; + private final boolean mIsFirstConfiguration; + private final List mLayoutConfigurations; - /** Manual selection by user */ - public static final int LAYOUT_SELECTION_CRITERIA_USER = 0; - - /** Auto-detection based on device provided language tag and layout type */ - public static final int LAYOUT_SELECTION_CRITERIA_DEVICE = 1; - - /** Auto-detection based on IME provided language tag and layout type */ - public static final int LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD = 2; - - KeyboardLayoutConfiguration(int keyboardLayoutType, - String keyboardLanguageTag, - KeyboardLayout keyboardLayout, - @LayoutSelectionCriteria int layoutSelectionCriteria) { - mKeyboardLayoutType = keyboardLayoutType; - mKeyboardLanguageTag = keyboardLanguageTag; - mKeyboardLayout = keyboardLayout; - mLayoutSelectionCriteria = layoutSelectionCriteria; - } - int getKeyboardLayoutType() { - return mKeyboardLayoutType; + private KeyboardConfigurationEvent(InputDevice inputDevice, boolean isFirstConfiguration, + List layoutConfigurations) { + mInputDevice = inputDevice; + mIsFirstConfiguration = isFirstConfiguration; + mLayoutConfigurations = layoutConfigurations; } - String getKeyboardLanguageTag() { - return mKeyboardLanguageTag; + public int getVendorId() { + return mInputDevice.getVendorId(); } - String getKeyboardLayoutName() { - return mKeyboardLayout.getLabel(); + public int getProductId() { + return mInputDevice.getProductId(); } - @LayoutSelectionCriteria int getLayoutSelectionCriteria() { - return mLayoutSelectionCriteria; + public boolean isFirstConfiguration() { + return mIsFirstConfiguration; } + + public List getLayoutConfigurations() { + return mLayoutConfigurations; + } + + @Override + public String toString() { + return "InputDevice = {VendorId = " + Integer.toHexString(getVendorId()) + + ", ProductId = " + Integer.toHexString(getProductId()) + + "}, isFirstConfiguration = " + mIsFirstConfiguration + + ", LayoutConfigurations = " + mLayoutConfigurations; + } + + /** + * Builder class to help create {@link KeyboardConfigurationEvent}. + */ + public static class Builder { + @NonNull + private final InputDevice mInputDevice; + private boolean mIsFirstConfiguration; + private final List mImeSubtypeList = new ArrayList<>(); + private final List mSelectedLayoutList = new ArrayList<>(); + private final List mLayoutSelectionCriteriaList = new ArrayList<>(); + + public Builder(@NonNull InputDevice inputDevice) { + Objects.requireNonNull(inputDevice, "InputDevice provided should not be null"); + mInputDevice = inputDevice; + } + + /** + * Set whether this is the first time this keyboard is configured. + */ + public Builder setIsFirstTimeConfiguration(boolean isFirstTimeConfiguration) { + mIsFirstConfiguration = isFirstTimeConfiguration; + return this; + } + + /** + * Adds keyboard layout configuration info for a particular IME subtype language + */ + public Builder addLayoutSelection(@NonNull InputMethodSubtype imeSubtype, + @Nullable KeyboardLayout selectedLayout, + @LayoutSelectionCriteria int layoutSelectionCriteria) { + Objects.requireNonNull(imeSubtype, "IME subtype provided should not be null"); + if (!isValidSelectionCriteria(layoutSelectionCriteria)) { + throw new IllegalStateException("Invalid layout selection criteria"); + } + mImeSubtypeList.add(imeSubtype); + mSelectedLayoutList.add(selectedLayout); + mLayoutSelectionCriteriaList.add(layoutSelectionCriteria); + return this; + } + + /** + * Creates {@link KeyboardConfigurationEvent} from the provided information + */ + public KeyboardConfigurationEvent build() { + int size = mImeSubtypeList.size(); + if (size == 0) { + throw new IllegalStateException("Should have at least one configuration"); + } + List configurationList = new ArrayList<>(); + for (int i = 0; i < size; i++) { + KeyboardLayout selectedLayout = mSelectedLayoutList.get(i); + @LayoutSelectionCriteria int layoutSelectionCriteria = + mLayoutSelectionCriteriaList.get(i); + InputMethodSubtype imeSubtype = mImeSubtypeList.get(i); + String keyboardLanguageTag; + String keyboardLayoutStringType; + if (layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_DEVICE) { + keyboardLanguageTag = mInputDevice.getKeyboardLanguageTag(); + keyboardLayoutStringType = mInputDevice.getKeyboardLayoutType(); + } else { + ULocale pkLocale = imeSubtype.getPhysicalKeyboardHintLanguageTag(); + keyboardLanguageTag = pkLocale != null ? pkLocale.toLanguageTag() + : imeSubtype.getCanonicalizedLanguageTag(); + keyboardLayoutStringType = imeSubtype.getPhysicalKeyboardHintLayoutType(); + } + // Sanitize null values + String keyboardLayoutName = + selectedLayout == null ? DEFAULT_LAYOUT : selectedLayout.getLabel(); + keyboardLanguageTag = keyboardLanguageTag == null ? "" : keyboardLanguageTag; + int keyboardLayoutType = KeyboardLayout.LayoutType.getLayoutTypeEnumValue( + keyboardLayoutStringType); + + configurationList.add( + new LayoutConfiguration(keyboardLayoutType, keyboardLanguageTag, + keyboardLayoutName, layoutSelectionCriteria)); + } + return new KeyboardConfigurationEvent(mInputDevice, mIsFirstConfiguration, + configurationList); + } + } + } + + @VisibleForTesting + static class LayoutConfiguration { + // This should match enum values defined in "frameworks/base/core/res/res/values/attrs.xml" + public final int keyboardLayoutType; + public final String keyboardLanguageTag; + public final String keyboardLayoutName; + @LayoutSelectionCriteria + public final int layoutSelectionCriteria; + + private LayoutConfiguration(int keyboardLayoutType, String keyboardLanguageTag, + String keyboardLayoutName, @LayoutSelectionCriteria int layoutSelectionCriteria) { + this.keyboardLayoutType = keyboardLayoutType; + this.keyboardLanguageTag = keyboardLanguageTag; + this.keyboardLayoutName = keyboardLayoutName; + this.layoutSelectionCriteria = layoutSelectionCriteria; + } + + @Override + public String toString() { + return "{keyboardLanguageTag = " + keyboardLanguageTag + " keyboardLayoutType = " + + KeyboardLayout.LayoutType.getLayoutNameFromValue(keyboardLayoutType) + + " keyboardLayoutName = " + keyboardLayoutName + " layoutSelectionCriteria = " + + getStringForSelectionCriteria(layoutSelectionCriteria) + "}"; + } + } + + private static String getStringForSelectionCriteria( + @LayoutSelectionCriteria int layoutSelectionCriteria) { + switch (layoutSelectionCriteria) { + case LAYOUT_SELECTION_CRITERIA_USER: + return "LAYOUT_SELECTION_CRITERIA_USER"; + case LAYOUT_SELECTION_CRITERIA_DEVICE: + return "LAYOUT_SELECTION_CRITERIA_DEVICE"; + case LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD: + return "LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD"; + default: + return "INVALID_CRITERIA"; + } + } + + private static boolean isValidSelectionCriteria(int layoutSelectionCriteria) { + return layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_USER + || layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_DEVICE + || layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD; } } diff --git a/services/core/java/com/android/server/input/PersistentDataStore.java b/services/core/java/com/android/server/input/PersistentDataStore.java index bce210d0a4a4f..31083fd5de03c 100644 --- a/services/core/java/com/android/server/input/PersistentDataStore.java +++ b/services/core/java/com/android/server/input/PersistentDataStore.java @@ -101,6 +101,10 @@ final class PersistentDataStore { } } + public boolean hasInputDeviceEntry(String inputDeviceDescriptor) { + return getInputDeviceState(inputDeviceDescriptor) != null; + } + public TouchCalibration getTouchCalibration(String inputDeviceDescriptor, int surfaceRotation) { InputDeviceState state = getInputDeviceState(inputDeviceDescriptor); if (state == null) { diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardMetricsCollectorTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardMetricsCollectorTests.kt new file mode 100644 index 0000000000000..c9724a3b43097 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/input/KeyboardMetricsCollectorTests.kt @@ -0,0 +1,179 @@ +/* + * Copyright 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.hardware.input.KeyboardLayout +import android.icu.util.ULocale +import android.platform.test.annotations.Presubmit +import android.view.InputDevice +import android.view.inputmethod.InputMethodSubtype +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Test + +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() + +private fun createImeSubtype( + imeSubtypeId: Int, + languageTag: String, + layoutType: String +): InputMethodSubtype = + InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(imeSubtypeId) + .setPhysicalKeyboardHint(ULocale.forLanguageTag(languageTag), layoutType).build() + +/** + * Tests for {@link KeyboardMetricsCollector}. + * + * Build/Install/Run: + * atest FrameworksServicesTests:KeyboardMetricsCollectorTests + */ +@Presubmit +class KeyboardMetricsCollectorTests { + + companion object { + const val DEVICE_ID = 1 + const val DEFAULT_VENDOR_ID = 123 + const val DEFAULT_PRODUCT_ID = 456 + } + + @Test + fun testCreateKeyboardConfigurationEvent_throwsExceptionWithoutAnyLayoutConfiguration() { + assertThrows(IllegalStateException::class.java) { + KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder( + createKeyboard( + DEVICE_ID, + DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, + null, + null + ) + ).build() + } + } + + @Test + fun testCreateKeyboardConfigurationEvent_throwsExceptionWithInvalidLayoutSelectionCriteria() { + assertThrows(IllegalStateException::class.java) { + KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder( + createKeyboard( + DEVICE_ID, + DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, + null, + null + ) + ).addLayoutSelection(createImeSubtype(1, "en-US", "qwerty"), null, 123).build() + } + } + + @Test + fun testCreateKeyboardConfigurationEvent_withMultipleConfigurations() { + val builder = KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder( + createKeyboard( + DEVICE_ID, + DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, + "de-CH", + "qwertz" + ) + ) + val event = builder.addLayoutSelection( + createImeSubtype(1, "en-US", "qwerty"), + KeyboardLayout(null, "English(US)(Qwerty)", null, 0, null, 0, 0, 0), + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD + ).addLayoutSelection( + createImeSubtype(2, "en-US", "azerty"), + null, + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_USER + ).addLayoutSelection( + createImeSubtype(3, "en-US", "qwerty"), + KeyboardLayout(null, "German", null, 0, null, 0, 0, 0), + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE + ).setIsFirstTimeConfiguration(true).build() + + assertEquals( + "KeyboardConfigurationEvent should pick vendor ID from provided InputDevice", + DEFAULT_VENDOR_ID, + event.vendorId + ) + assertEquals( + "KeyboardConfigurationEvent should pick product ID from provided InputDevice", + DEFAULT_PRODUCT_ID, + event.productId + ) + assertTrue(event.isFirstConfiguration) + + assertEquals( + "KeyboardConfigurationEvent should contain 3 configurations provided", + 3, + event.layoutConfigurations.size + ) + assertExpectedLayoutConfiguration( + event.layoutConfigurations[0], + "en-US", + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwerty"), + "English(US)(Qwerty)", + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD + ) + assertExpectedLayoutConfiguration( + event.layoutConfigurations[1], + "en-US", + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("azerty"), + KeyboardMetricsCollector.DEFAULT_LAYOUT, + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_USER + ) + assertExpectedLayoutConfiguration( + event.layoutConfigurations[2], + "de-CH", + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwertz"), + "German", + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE + ) + } + + private fun assertExpectedLayoutConfiguration( + configuration: KeyboardMetricsCollector.LayoutConfiguration, + expectedLanguageTag: String, + expectedLayoutType: Int, + expectedSelectedLayout: String, + expectedLayoutSelectionCriteria: Int + ) { + assertEquals(expectedLanguageTag, configuration.keyboardLanguageTag) + assertEquals(expectedLayoutType, configuration.keyboardLayoutType) + assertEquals(expectedSelectedLayout, configuration.keyboardLayoutName) + assertEquals(expectedLayoutSelectionCriteria, configuration.layoutSelectionCriteria) + } +} \ No newline at end of file