1 /* 2 * Copyright 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.input; 18 19 import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_USER; 20 import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE; 21 import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD; 22 import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEFAULT; 23 import static android.hardware.input.KeyboardLayoutSelectionResult.layoutSelectionCriteriaToString; 24 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.hardware.input.KeyboardLayout; 28 import android.hardware.input.KeyboardLayoutSelectionResult.LayoutSelectionCriteria; 29 import android.icu.util.ULocale; 30 import android.text.TextUtils; 31 import android.util.Log; 32 import android.util.Slog; 33 import android.util.proto.ProtoOutputStream; 34 import android.view.InputDevice; 35 import android.view.inputmethod.InputMethodSubtype; 36 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.internal.os.KeyboardConfiguredProto.KeyboardLayoutConfig; 39 import com.android.internal.os.KeyboardConfiguredProto.RepeatedKeyboardLayoutConfig; 40 import com.android.internal.util.FrameworkStatsLog; 41 42 import java.util.ArrayList; 43 import java.util.Arrays; 44 import java.util.List; 45 import java.util.Objects; 46 47 /** 48 * Collect Keyboard metrics 49 */ 50 public final class KeyboardMetricsCollector { 51 private static final String TAG = "KeyboardMetricCollector"; 52 53 // To enable these logs, run: 'adb shell setprop log.tag.KeyboardMetricCollector DEBUG' 54 // (requires restart) 55 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 56 57 @VisibleForTesting 58 static final String DEFAULT_LAYOUT_NAME = "Default"; 59 60 @VisibleForTesting 61 public static final String DEFAULT_LANGUAGE_TAG = "None"; 62 63 private static final int INVALID_SYSTEMS_EVENT = FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__UNSPECIFIED; 64 65 /** 66 * Log keyboard system shortcuts for the proto 67 * {@link com.android.os.input.KeyboardSystemsEventReported} 68 * defined in "stats/atoms/input/input_extension_atoms.proto" 69 */ logKeyboardSystemsEventReportedAtom(@onNull InputDevice inputDevice, int[] keycodes, int modifierState, int systemsEvent)70 public static void logKeyboardSystemsEventReportedAtom(@NonNull InputDevice inputDevice, 71 int[] keycodes, int modifierState, int systemsEvent) { 72 if (systemsEvent == INVALID_SYSTEMS_EVENT || inputDevice.isVirtual() 73 || !inputDevice.isFullKeyboard()) { 74 return; 75 } 76 FrameworkStatsLog.write(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED, 77 inputDevice.getVendorId(), inputDevice.getProductId(), 78 systemsEvent, keycodes, modifierState, inputDevice.getDeviceBus()); 79 80 if (DEBUG) { 81 Slog.d(TAG, "Logging Keyboard system event: " + modifierState + " + " + Arrays.toString( 82 keycodes) + " -> " + systemsEvent); 83 } 84 } 85 86 /** 87 * Function to log the KeyboardConfigured 88 * {@link com.android.os.input.KeyboardConfigured} atom 89 * 90 * @param event {@link KeyboardConfigurationEvent} contains information about keyboard 91 * configuration. Use {@link KeyboardConfigurationEvent.Builder} to create the 92 * configuration event to log. 93 */ logKeyboardConfiguredAtom(KeyboardConfigurationEvent event)94 public static void logKeyboardConfiguredAtom(KeyboardConfigurationEvent event) { 95 // Creating proto to log nested field KeyboardLayoutConfig in atom 96 ProtoOutputStream proto = new ProtoOutputStream(); 97 98 for (LayoutConfiguration layoutConfiguration : event.getLayoutConfigurations()) { 99 addKeyboardLayoutConfigurationToProto(proto, layoutConfiguration); 100 } 101 // Push the atom to Statsd 102 FrameworkStatsLog.write(FrameworkStatsLog.KEYBOARD_CONFIGURED, 103 event.isFirstConfiguration(), event.getVendorId(), event.getProductId(), 104 proto.getBytes(), event.getDeviceBus()); 105 106 if (DEBUG) { 107 Slog.d(TAG, "Logging Keyboard configuration event: " + event); 108 } 109 } 110 111 /** 112 * Populate the KeyboardLayoutConfig proto which is a repeated proto 113 * in the RepeatedKeyboardLayoutConfig proto with values from the 114 * {@link LayoutConfiguration} class 115 * The proto definitions can be found at: 116 * "frameworks/proto_logging/stats/atoms/input/input_extension_atoms.proto" 117 * 118 * @param proto Representing the nested proto RepeatedKeyboardLayoutConfig 119 * @param layoutConfiguration Class containing the fields for populating the 120 * KeyboardLayoutConfig proto 121 */ addKeyboardLayoutConfigurationToProto(ProtoOutputStream proto, LayoutConfiguration layoutConfiguration)122 private static void addKeyboardLayoutConfigurationToProto(ProtoOutputStream proto, 123 LayoutConfiguration layoutConfiguration) { 124 // Start a new KeyboardLayoutConfig proto. 125 long keyboardLayoutConfigToken = proto.start( 126 RepeatedKeyboardLayoutConfig.KEYBOARD_LAYOUT_CONFIG); 127 proto.write(KeyboardLayoutConfig.KEYBOARD_LANGUAGE_TAG, 128 layoutConfiguration.keyboardLanguageTag); 129 proto.write(KeyboardLayoutConfig.KEYBOARD_LAYOUT_TYPE, 130 layoutConfiguration.keyboardLayoutType); 131 proto.write(KeyboardLayoutConfig.KEYBOARD_LAYOUT_NAME, 132 layoutConfiguration.keyboardLayoutName); 133 proto.write(KeyboardLayoutConfig.LAYOUT_SELECTION_CRITERIA, 134 layoutConfiguration.layoutSelectionCriteria); 135 proto.write(KeyboardLayoutConfig.IME_LANGUAGE_TAG, 136 layoutConfiguration.imeLanguageTag); 137 proto.write(KeyboardLayoutConfig.IME_LAYOUT_TYPE, 138 layoutConfiguration.imeLayoutType); 139 proto.end(keyboardLayoutConfigToken); 140 } 141 142 /** 143 * Class representing the proto KeyboardLayoutConfig defined in 144 * "frameworks/proto_logging/stats/atoms/input/input_extension_atoms.proto 145 * 146 * @see com.android.os.input.KeyboardConfigured 147 */ 148 public static class KeyboardConfigurationEvent { 149 150 private final InputDevice mInputDevice; 151 private final boolean mIsFirstConfiguration; 152 private final List<LayoutConfiguration> mLayoutConfigurations; 153 KeyboardConfigurationEvent(InputDevice inputDevice, boolean isFirstConfiguration, List<LayoutConfiguration> layoutConfigurations)154 private KeyboardConfigurationEvent(InputDevice inputDevice, boolean isFirstConfiguration, 155 List<LayoutConfiguration> layoutConfigurations) { 156 mInputDevice = inputDevice; 157 mIsFirstConfiguration = isFirstConfiguration; 158 mLayoutConfigurations = layoutConfigurations; 159 } 160 getVendorId()161 public int getVendorId() { 162 return mInputDevice.getVendorId(); 163 } 164 getProductId()165 public int getProductId() { 166 return mInputDevice.getProductId(); 167 } 168 getDeviceBus()169 public int getDeviceBus() { 170 return mInputDevice.getDeviceBus(); 171 } 172 isFirstConfiguration()173 public boolean isFirstConfiguration() { 174 return mIsFirstConfiguration; 175 } 176 getLayoutConfigurations()177 public List<LayoutConfiguration> getLayoutConfigurations() { 178 return mLayoutConfigurations; 179 } 180 181 @Override toString()182 public String toString() { 183 return "InputDevice = {VendorId = " + Integer.toHexString(getVendorId()) 184 + ", ProductId = " + Integer.toHexString(getProductId()) 185 + ", Device Bus = " + Integer.toHexString(getDeviceBus()) 186 + "}, isFirstConfiguration = " + mIsFirstConfiguration 187 + ", LayoutConfigurations = " + mLayoutConfigurations; 188 } 189 190 /** 191 * Builder class to help create {@link KeyboardConfigurationEvent}. 192 */ 193 public static class Builder { 194 @NonNull 195 private final InputDevice mInputDevice; 196 private boolean mIsFirstConfiguration; 197 private final List<InputMethodSubtype> mImeSubtypeList = new ArrayList<>(); 198 private final List<String> mSelectedLayoutList = new ArrayList<>(); 199 private final List<Integer> mLayoutSelectionCriteriaList = new ArrayList<>(); 200 Builder(@onNull InputDevice inputDevice)201 public Builder(@NonNull InputDevice inputDevice) { 202 Objects.requireNonNull(inputDevice, "InputDevice provided should not be null"); 203 mInputDevice = inputDevice; 204 } 205 206 /** 207 * Set whether this is the first time this keyboard is configured. 208 */ setIsFirstTimeConfiguration(boolean isFirstTimeConfiguration)209 public Builder setIsFirstTimeConfiguration(boolean isFirstTimeConfiguration) { 210 mIsFirstConfiguration = isFirstTimeConfiguration; 211 return this; 212 } 213 214 /** 215 * Adds keyboard layout configuration info for a particular IME subtype language 216 */ addLayoutSelection(@onNull InputMethodSubtype imeSubtype, @Nullable String selectedLayout, @LayoutSelectionCriteria int layoutSelectionCriteria)217 public Builder addLayoutSelection(@NonNull InputMethodSubtype imeSubtype, 218 @Nullable String selectedLayout, 219 @LayoutSelectionCriteria int layoutSelectionCriteria) { 220 Objects.requireNonNull(imeSubtype, "IME subtype provided should not be null"); 221 if (!isValidSelectionCriteria(layoutSelectionCriteria)) { 222 throw new IllegalStateException("Invalid layout selection criteria"); 223 } 224 mImeSubtypeList.add(imeSubtype); 225 mSelectedLayoutList.add(selectedLayout); 226 mLayoutSelectionCriteriaList.add(layoutSelectionCriteria); 227 return this; 228 } 229 230 /** 231 * Creates {@link KeyboardConfigurationEvent} from the provided information 232 */ build()233 public KeyboardConfigurationEvent build() { 234 int size = mImeSubtypeList.size(); 235 if (size == 0) { 236 throw new IllegalStateException("Should have at least one configuration"); 237 } 238 List<LayoutConfiguration> configurationList = new ArrayList<>(); 239 for (int i = 0; i < size; i++) { 240 @LayoutSelectionCriteria int layoutSelectionCriteria = 241 mLayoutSelectionCriteriaList.get(i); 242 InputMethodSubtype imeSubtype = mImeSubtypeList.get(i); 243 String keyboardLanguageTag = mInputDevice.getKeyboardLanguageTag(); 244 keyboardLanguageTag = TextUtils.isEmpty(keyboardLanguageTag) 245 ? DEFAULT_LANGUAGE_TAG : keyboardLanguageTag; 246 int keyboardLayoutType = KeyboardLayout.LayoutType.getLayoutTypeEnumValue( 247 mInputDevice.getKeyboardLayoutType()); 248 249 ULocale pkLocale = imeSubtype.getPhysicalKeyboardHintLanguageTag(); 250 String imeLanguageTag = pkLocale != null ? pkLocale.toLanguageTag() 251 : imeSubtype.getCanonicalizedLanguageTag(); 252 imeLanguageTag = TextUtils.isEmpty(imeLanguageTag) ? DEFAULT_LANGUAGE_TAG 253 : imeLanguageTag; 254 int imeLayoutType = KeyboardLayout.LayoutType.getLayoutTypeEnumValue( 255 imeSubtype.getPhysicalKeyboardHintLayoutType()); 256 257 // Sanitize null values 258 String keyboardLayoutName = mSelectedLayoutList.get(i) == null 259 ? DEFAULT_LAYOUT_NAME 260 : mSelectedLayoutList.get(i); 261 262 configurationList.add( 263 new LayoutConfiguration(keyboardLayoutType, keyboardLanguageTag, 264 keyboardLayoutName, layoutSelectionCriteria, 265 imeLayoutType, imeLanguageTag)); 266 } 267 return new KeyboardConfigurationEvent(mInputDevice, mIsFirstConfiguration, 268 configurationList); 269 } 270 } 271 } 272 273 @VisibleForTesting 274 static class LayoutConfiguration { 275 // This should match enum values defined in "frameworks/base/core/res/res/values/attrs.xml" 276 public final int keyboardLayoutType; 277 public final String keyboardLanguageTag; 278 public final String keyboardLayoutName; 279 @LayoutSelectionCriteria 280 public final int layoutSelectionCriteria; 281 public final int imeLayoutType; 282 public final String imeLanguageTag; 283 LayoutConfiguration(int keyboardLayoutType, String keyboardLanguageTag, String keyboardLayoutName, @LayoutSelectionCriteria int layoutSelectionCriteria, int imeLayoutType, String imeLanguageTag)284 private LayoutConfiguration(int keyboardLayoutType, String keyboardLanguageTag, 285 String keyboardLayoutName, @LayoutSelectionCriteria int layoutSelectionCriteria, 286 int imeLayoutType, String imeLanguageTag) { 287 this.keyboardLayoutType = keyboardLayoutType; 288 this.keyboardLanguageTag = keyboardLanguageTag; 289 this.keyboardLayoutName = keyboardLayoutName; 290 this.layoutSelectionCriteria = layoutSelectionCriteria; 291 this.imeLayoutType = imeLayoutType; 292 this.imeLanguageTag = imeLanguageTag; 293 } 294 295 @Override toString()296 public String toString() { 297 return "{keyboardLanguageTag = " + keyboardLanguageTag 298 + " keyboardLayoutType = " 299 + KeyboardLayout.LayoutType.getLayoutNameFromValue(keyboardLayoutType) 300 + " keyboardLayoutName = " + keyboardLayoutName 301 + " layoutSelectionCriteria = " 302 + layoutSelectionCriteriaToString(layoutSelectionCriteria) 303 + " imeLanguageTag = " + imeLanguageTag 304 + " imeLayoutType = " + KeyboardLayout.LayoutType.getLayoutNameFromValue( 305 imeLayoutType) 306 + "}"; 307 } 308 } 309 isValidSelectionCriteria(int layoutSelectionCriteria)310 private static boolean isValidSelectionCriteria(int layoutSelectionCriteria) { 311 return layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_USER 312 || layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_DEVICE 313 || layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD 314 || layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_DEFAULT; 315 } 316 } 317