Merge "Log KeyboardConfigured atom" into udc-qpr-dev

This commit is contained in:
Vaibhav Devmurari
2023-06-13 13:57:26 +00:00
committed by Android (Google) Code Review
5 changed files with 543 additions and 120 deletions

View File

@@ -73,7 +73,7 @@ public final class KeyboardLayout implements Parcelable, Comparable<KeyboardLayo
private final int mProductId;
/** Currently supported Layout types in the KCM files */
private enum LayoutType {
public enum LayoutType {
UNDEFINED(0, LAYOUT_TYPE_UNDEFINED),
QWERTY(1, LAYOUT_TYPE_QWERTY),
QWERTZ(2, LAYOUT_TYPE_QWERTZ),
@@ -88,9 +88,11 @@ public final class KeyboardLayout implements Parcelable, Comparable<KeyboardLayo
private final int mValue;
private final String mName;
private static final Map<Integer, LayoutType> VALUE_TO_ENUM_MAP = new HashMap<>();
private static final Map<String, LayoutType> 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<KeyboardLayo
private String getName() {
return mName;
}
/**
* Returns enum value for provided layout type
* @param layoutName name of the layout type
* @return int value corresponding to the LayoutType enum that matches the layout name.
* (LayoutType.UNDEFINED if no match found)
*/
public static int getLayoutTypeEnumValue(String layoutName) {
return NAME_TO_ENUM_MAP.getOrDefault(layoutName, UNDEFINED).getValue();
}
/**
* Returns name for provided layout type enum value
* @param enumValue value representation for LayoutType enum
* @return Layout name corresponding to the enum value (LAYOUT_TYPE_UNDEFINED if not found)
*/
public static String getLayoutNameFromValue(int enumValue) {
return VALUE_TO_ENUM_MAP.getOrDefault(enumValue, UNDEFINED).getName();
}
}
@NonNull

View File

@@ -16,6 +16,10 @@
package com.android.server.input;
import static com.android.server.input.KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE;
import static com.android.server.input.KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_USER;
import static com.android.server.input.KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD;
import android.annotation.AnyThread;
import android.annotation.MainThread;
import android.annotation.NonNull;
@@ -67,6 +71,8 @@ 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;
import com.android.server.input.KeyboardMetricsCollector.KeyboardConfigurationEvent;
import com.android.server.input.KeyboardMetricsCollector.LayoutSelectionCriteria;
import com.android.server.inputmethod.InputMethodManagerInternal;
import libcore.io.Streams;
@@ -118,7 +124,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
// 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<>();
private final Map<String, KeyboardLayoutInfo> 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<String> selectedLayouts = new HashSet<>();
for (ImeInfo imeInfo : getImeInfoListForLayoutMapping()) {
List<ImeInfo> imeInfoList = getImeInfoListForLayoutMapping();
List<KeyboardLayoutInfo> 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 <VendorID, ProductID> 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<ImeInfo> imeInfoList, @NonNull List<KeyboardLayoutInfo> 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);

View File

@@ -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<KeyboardLayoutConfiguration> 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<LayoutConfiguration> 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<LayoutConfiguration> 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<LayoutConfiguration> 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<InputMethodSubtype> mImeSubtypeList = new ArrayList<>();
private final List<KeyboardLayout> mSelectedLayoutList = new ArrayList<>();
private final List<Integer> 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<LayoutConfiguration> 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;
}
}

View File

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

View File

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