blob: 8721ef7687a4db237e22b8519ceadd9dea4ee557 [file] [log] [blame]
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +00001/*
2 * Copyright 2024 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//! Contains the KeyboardClassifier, that tries to identify whether an Input device is an
18//! alphabetic or non-alphabetic keyboard. It also tracks the KeyEvents produced by the device
19//! in order to verify/change the inferred keyboard type.
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +000020//!
21//! Initial classification:
22//! - If DeviceClass includes Dpad, Touch, Cursor, MultiTouch, ExternalStylus, Touchpad, Dpad,
23//! Gamepad, Switch, Joystick, RotaryEncoder => KeyboardType::NonAlphabetic
24//! - Otherwise if DeviceClass has Keyboard and not AlphabeticKey => KeyboardType::NonAlphabetic
25//! - Otherwise if DeviceClass has both Keyboard and AlphabeticKey => KeyboardType::Alphabetic
26//!
27//! On process keys:
28//! - If KeyboardType::NonAlphabetic and we receive alphabetic key event, then change type to
29//! KeyboardType::Alphabetic. Once changed, no further changes. (i.e. verified = true)
30//! - TODO(b/263559234): If KeyboardType::Alphabetic and we don't receive any alphabetic key event
31//! across multiple device connections in a time period, then change type to
32//! KeyboardType::NonAlphabetic. Once changed, it can still change back to Alphabetic
33//! (i.e. verified = false).
34//!
35//! TODO(b/263559234): Data store implementation to store information about past classification
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000036
37use crate::input::{DeviceId, InputDevice, KeyboardType};
Vaibhav Devmuraria8359fc2024-06-10 20:01:25 +000038use crate::keyboard_classification_config::CLASSIFIED_DEVICES;
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +000039use crate::{DeviceClass, ModifierState};
40use std::collections::HashMap;
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000041
42/// The KeyboardClassifier is used to classify a keyboard device into non-keyboard, alphabetic
43/// keyboard or non-alphabetic keyboard
44#[derive(Default)]
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +000045pub struct KeyboardClassifier {
46 device_map: HashMap<DeviceId, KeyboardInfo>,
47}
48
49struct KeyboardInfo {
50 _device: InputDevice,
51 keyboard_type: KeyboardType,
52 is_finalized: bool,
53}
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000054
55impl KeyboardClassifier {
56 /// Create a new KeyboardClassifier
57 pub fn new() -> Self {
58 Default::default()
59 }
60
61 /// Adds keyboard to KeyboardClassifier
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +000062 pub fn notify_keyboard_changed(&mut self, device: InputDevice) {
63 let (keyboard_type, is_finalized) = self.classify_keyboard(&device);
64 self.device_map.insert(
65 device.device_id,
66 KeyboardInfo { _device: device, keyboard_type, is_finalized },
67 );
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000068 }
69
70 /// Get keyboard type for a tracked keyboard in KeyboardClassifier
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +000071 pub fn get_keyboard_type(&self, device_id: DeviceId) -> KeyboardType {
72 return if let Some(keyboard) = self.device_map.get(&device_id) {
73 keyboard.keyboard_type
74 } else {
75 KeyboardType::None
76 };
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000077 }
78
79 /// Tells if keyboard type classification is finalized. Once finalized the classification can't
80 /// change until device is reconnected again.
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +000081 ///
82 /// Finalized devices are either "alphabetic" keyboards or keyboards in blocklist or
83 /// allowlist that are explicitly categorized and won't change with future key events
84 pub fn is_finalized(&self, device_id: DeviceId) -> bool {
85 return if let Some(keyboard) = self.device_map.get(&device_id) {
86 keyboard.is_finalized
87 } else {
88 false
89 };
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000090 }
91
92 /// Process a key event and change keyboard type if required.
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +000093 /// - If any key event occurs, the keyboard type will change from None to NonAlphabetic
94 /// - If an alphabetic key occurs, the keyboard type will change to Alphabetic
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000095 pub fn process_key(
96 &mut self,
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +000097 device_id: DeviceId,
98 evdev_code: i32,
99 modifier_state: ModifierState,
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +0000100 ) {
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +0000101 if let Some(keyboard) = self.device_map.get_mut(&device_id) {
102 // Ignore all key events with modifier state since they can be macro shortcuts used by
103 // some non-keyboard peripherals like TV remotes, game controllers, etc.
104 if modifier_state.bits() != 0 {
105 return;
106 }
107 if Self::is_alphabetic_key(&evdev_code) {
108 keyboard.keyboard_type = KeyboardType::Alphabetic;
109 keyboard.is_finalized = true;
110 }
111 }
112 }
113
114 fn classify_keyboard(&self, device: &InputDevice) -> (KeyboardType, bool) {
115 // This should never happen but having keyboard device class is necessary to be classified
116 // as any type of keyboard.
117 if !device.classes.contains(DeviceClass::Keyboard) {
118 return (KeyboardType::None, true);
119 }
120 // Normal classification for internal and virtual keyboards
121 if !device.classes.contains(DeviceClass::External)
122 || device.classes.contains(DeviceClass::Virtual)
123 {
124 return if device.classes.contains(DeviceClass::AlphabeticKey) {
125 (KeyboardType::Alphabetic, true)
126 } else {
127 (KeyboardType::NonAlphabetic, true)
128 };
129 }
Vaibhav Devmuraria8359fc2024-06-10 20:01:25 +0000130
131 // Check in known device list for classification
132 for data in CLASSIFIED_DEVICES.iter() {
133 if device.identifier.vendor == data.0 && device.identifier.product == data.1 {
134 return (data.2, data.3);
135 }
136 }
137
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +0000138 // Any composite device with multiple device classes should be categorized as non-alphabetic
139 // keyboard initially
140 if device.classes.contains(DeviceClass::Touch)
141 || device.classes.contains(DeviceClass::Cursor)
142 || device.classes.contains(DeviceClass::MultiTouch)
143 || device.classes.contains(DeviceClass::ExternalStylus)
144 || device.classes.contains(DeviceClass::Touchpad)
145 || device.classes.contains(DeviceClass::Dpad)
146 || device.classes.contains(DeviceClass::Gamepad)
147 || device.classes.contains(DeviceClass::Switch)
148 || device.classes.contains(DeviceClass::Joystick)
149 || device.classes.contains(DeviceClass::RotaryEncoder)
150 {
151 // If categorized as NonAlphabetic and no device class AlphabeticKey reported by the
152 // kernel, we no longer need to process key events to verify.
153 return (
154 KeyboardType::NonAlphabetic,
155 !device.classes.contains(DeviceClass::AlphabeticKey),
156 );
157 }
158 // Only devices with "Keyboard" and "AlphabeticKey" should be classified as full keyboard
159 if device.classes.contains(DeviceClass::AlphabeticKey) {
160 (KeyboardType::Alphabetic, true)
161 } else {
162 // If categorized as NonAlphabetic and no device class AlphabeticKey reported by the
163 // kernel, we no longer need to process key events to verify.
164 (KeyboardType::NonAlphabetic, true)
165 }
166 }
167
168 fn is_alphabetic_key(evdev_code: &i32) -> bool {
169 // Keyboard alphabetic row 1 (Q W E R T Y U I O P [ ])
170 (16..=27).contains(evdev_code)
171 // Keyboard alphabetic row 2 (A S D F G H J K L ; ' `)
172 || (30..=41).contains(evdev_code)
173 // Keyboard alphabetic row 3 (\ Z X C V B N M , . /)
174 || (43..=53).contains(evdev_code)
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use crate::input::{DeviceId, InputDevice, KeyboardType};
Vaibhav Devmuraria8359fc2024-06-10 20:01:25 +0000181 use crate::keyboard_classification_config::CLASSIFIED_DEVICES;
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +0000182 use crate::keyboard_classifier::KeyboardClassifier;
183 use crate::{DeviceClass, ModifierState, RustInputDeviceIdentifier};
184
185 static DEVICE_ID: DeviceId = DeviceId(1);
186 static KEY_A: i32 = 30;
187 static KEY_1: i32 = 2;
188
189 #[test]
190 fn classify_external_alphabetic_keyboard() {
191 let mut classifier = KeyboardClassifier::new();
192 classifier.notify_keyboard_changed(create_device(
193 DeviceClass::Keyboard | DeviceClass::AlphabeticKey | DeviceClass::External,
194 ));
195 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::Alphabetic);
196 assert!(classifier.is_finalized(DEVICE_ID));
197 }
198
199 #[test]
200 fn classify_external_non_alphabetic_keyboard() {
201 let mut classifier = KeyboardClassifier::new();
202 classifier
203 .notify_keyboard_changed(create_device(DeviceClass::Keyboard | DeviceClass::External));
204 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
205 assert!(classifier.is_finalized(DEVICE_ID));
206 }
207
208 #[test]
209 fn classify_mouse_pretending_as_keyboard() {
210 let mut classifier = KeyboardClassifier::new();
211 classifier.notify_keyboard_changed(create_device(
212 DeviceClass::Keyboard
213 | DeviceClass::Cursor
214 | DeviceClass::AlphabeticKey
215 | DeviceClass::External,
216 ));
217 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
218 assert!(!classifier.is_finalized(DEVICE_ID));
219 }
220
221 #[test]
222 fn classify_touchpad_pretending_as_keyboard() {
223 let mut classifier = KeyboardClassifier::new();
224 classifier.notify_keyboard_changed(create_device(
225 DeviceClass::Keyboard
226 | DeviceClass::Touchpad
227 | DeviceClass::AlphabeticKey
228 | DeviceClass::External,
229 ));
230 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
231 assert!(!classifier.is_finalized(DEVICE_ID));
232 }
233
234 #[test]
235 fn classify_stylus_pretending_as_keyboard() {
236 let mut classifier = KeyboardClassifier::new();
237 classifier.notify_keyboard_changed(create_device(
238 DeviceClass::Keyboard
239 | DeviceClass::ExternalStylus
240 | DeviceClass::AlphabeticKey
241 | DeviceClass::External,
242 ));
243 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
244 assert!(!classifier.is_finalized(DEVICE_ID));
245 }
246
247 #[test]
248 fn classify_dpad_pretending_as_keyboard() {
249 let mut classifier = KeyboardClassifier::new();
250 classifier.notify_keyboard_changed(create_device(
251 DeviceClass::Keyboard
252 | DeviceClass::Dpad
253 | DeviceClass::AlphabeticKey
254 | DeviceClass::External,
255 ));
256 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
257 assert!(!classifier.is_finalized(DEVICE_ID));
258 }
259
260 #[test]
261 fn classify_joystick_pretending_as_keyboard() {
262 let mut classifier = KeyboardClassifier::new();
263 classifier.notify_keyboard_changed(create_device(
264 DeviceClass::Keyboard
265 | DeviceClass::Joystick
266 | DeviceClass::AlphabeticKey
267 | DeviceClass::External,
268 ));
269 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
270 assert!(!classifier.is_finalized(DEVICE_ID));
271 }
272
273 #[test]
274 fn classify_gamepad_pretending_as_keyboard() {
275 let mut classifier = KeyboardClassifier::new();
276 classifier.notify_keyboard_changed(create_device(
277 DeviceClass::Keyboard
278 | DeviceClass::Gamepad
279 | DeviceClass::AlphabeticKey
280 | DeviceClass::External,
281 ));
282 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
283 assert!(!classifier.is_finalized(DEVICE_ID));
284 }
285
286 #[test]
287 fn reclassify_keyboard_on_alphabetic_key_event() {
288 let mut classifier = KeyboardClassifier::new();
289 classifier.notify_keyboard_changed(create_device(
290 DeviceClass::Keyboard
291 | DeviceClass::Dpad
292 | DeviceClass::AlphabeticKey
293 | DeviceClass::External,
294 ));
295 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
296 assert!(!classifier.is_finalized(DEVICE_ID));
297
298 // on alphabetic key event
299 classifier.process_key(DEVICE_ID, KEY_A, ModifierState::None);
300 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::Alphabetic);
301 assert!(classifier.is_finalized(DEVICE_ID));
302 }
303
304 #[test]
305 fn dont_reclassify_keyboard_on_non_alphabetic_key_event() {
306 let mut classifier = KeyboardClassifier::new();
307 classifier.notify_keyboard_changed(create_device(
308 DeviceClass::Keyboard
309 | DeviceClass::Dpad
310 | DeviceClass::AlphabeticKey
311 | DeviceClass::External,
312 ));
313 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
314 assert!(!classifier.is_finalized(DEVICE_ID));
315
316 // on number key event
317 classifier.process_key(DEVICE_ID, KEY_1, ModifierState::None);
318 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
319 assert!(!classifier.is_finalized(DEVICE_ID));
320 }
321
322 #[test]
323 fn dont_reclassify_keyboard_on_alphabetic_key_event_with_modifiers() {
324 let mut classifier = KeyboardClassifier::new();
325 classifier.notify_keyboard_changed(create_device(
326 DeviceClass::Keyboard
327 | DeviceClass::Dpad
328 | DeviceClass::AlphabeticKey
329 | DeviceClass::External,
330 ));
331 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
332 assert!(!classifier.is_finalized(DEVICE_ID));
333
334 classifier.process_key(DEVICE_ID, KEY_A, ModifierState::CtrlOn);
335 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
336 assert!(!classifier.is_finalized(DEVICE_ID));
337 }
338
Vaibhav Devmuraria8359fc2024-06-10 20:01:25 +0000339 #[test]
340 fn classify_known_devices() {
341 let mut classifier = KeyboardClassifier::new();
342 for device in CLASSIFIED_DEVICES.iter() {
343 classifier
344 .notify_keyboard_changed(create_device_with_vendor_product_ids(device.0, device.1));
345 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), device.2);
346 assert_eq!(classifier.is_finalized(DEVICE_ID), device.3);
347 }
348 }
349
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +0000350 fn create_device(classes: DeviceClass) -> InputDevice {
351 InputDevice {
352 device_id: DEVICE_ID,
353 identifier: RustInputDeviceIdentifier {
354 name: "test_device".to_string(),
355 location: "location".to_string(),
356 unique_id: "unique_id".to_string(),
357 bus: 123,
358 vendor: 234,
359 product: 345,
360 version: 567,
361 descriptor: "descriptor".to_string(),
362 },
363 classes,
364 }
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +0000365 }
Vaibhav Devmuraria8359fc2024-06-10 20:01:25 +0000366
367 fn create_device_with_vendor_product_ids(vendor: u16, product: u16) -> InputDevice {
368 InputDevice {
369 device_id: DEVICE_ID,
370 identifier: RustInputDeviceIdentifier {
371 name: "test_device".to_string(),
372 location: "location".to_string(),
373 unique_id: "unique_id".to_string(),
374 bus: 123,
375 vendor,
376 product,
377 version: 567,
378 descriptor: "descriptor".to_string(),
379 },
380 classes: DeviceClass::Keyboard | DeviceClass::AlphabeticKey | DeviceClass::External,
381 }
382 }
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +0000383}