• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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