blob: 1063fac664efafa2d3dd8c1ac974ff064d9ac336 [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 Devmurari2e73b2a2024-06-07 17:45:19 +000038use crate::{DeviceClass, ModifierState};
39use std::collections::HashMap;
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000040
41/// The KeyboardClassifier is used to classify a keyboard device into non-keyboard, alphabetic
42/// keyboard or non-alphabetic keyboard
43#[derive(Default)]
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +000044pub struct KeyboardClassifier {
45 device_map: HashMap<DeviceId, KeyboardInfo>,
46}
47
48struct KeyboardInfo {
49 _device: InputDevice,
50 keyboard_type: KeyboardType,
51 is_finalized: bool,
52}
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000053
54impl KeyboardClassifier {
55 /// Create a new KeyboardClassifier
56 pub fn new() -> Self {
57 Default::default()
58 }
59
60 /// Adds keyboard to KeyboardClassifier
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +000061 pub fn notify_keyboard_changed(&mut self, device: InputDevice) {
62 let (keyboard_type, is_finalized) = self.classify_keyboard(&device);
63 self.device_map.insert(
64 device.device_id,
65 KeyboardInfo { _device: device, keyboard_type, is_finalized },
66 );
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000067 }
68
69 /// Get keyboard type for a tracked keyboard in KeyboardClassifier
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +000070 pub fn get_keyboard_type(&self, device_id: DeviceId) -> KeyboardType {
71 return if let Some(keyboard) = self.device_map.get(&device_id) {
72 keyboard.keyboard_type
73 } else {
74 KeyboardType::None
75 };
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000076 }
77
78 /// Tells if keyboard type classification is finalized. Once finalized the classification can't
79 /// change until device is reconnected again.
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +000080 ///
81 /// Finalized devices are either "alphabetic" keyboards or keyboards in blocklist or
82 /// allowlist that are explicitly categorized and won't change with future key events
83 pub fn is_finalized(&self, device_id: DeviceId) -> bool {
84 return if let Some(keyboard) = self.device_map.get(&device_id) {
85 keyboard.is_finalized
86 } else {
87 false
88 };
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000089 }
90
91 /// Process a key event and change keyboard type if required.
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +000092 /// - If any key event occurs, the keyboard type will change from None to NonAlphabetic
93 /// - If an alphabetic key occurs, the keyboard type will change to Alphabetic
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000094 pub fn process_key(
95 &mut self,
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +000096 device_id: DeviceId,
97 evdev_code: i32,
98 modifier_state: ModifierState,
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +000099 ) {
Vaibhav Devmurari2e73b2a2024-06-07 17:45:19 +0000100 if let Some(keyboard) = self.device_map.get_mut(&device_id) {
101 // Ignore all key events with modifier state since they can be macro shortcuts used by
102 // some non-keyboard peripherals like TV remotes, game controllers, etc.
103 if modifier_state.bits() != 0 {
104 return;
105 }
106 if Self::is_alphabetic_key(&evdev_code) {
107 keyboard.keyboard_type = KeyboardType::Alphabetic;
108 keyboard.is_finalized = true;
109 }
110 }
111 }
112
113 fn classify_keyboard(&self, device: &InputDevice) -> (KeyboardType, bool) {
114 // This should never happen but having keyboard device class is necessary to be classified
115 // as any type of keyboard.
116 if !device.classes.contains(DeviceClass::Keyboard) {
117 return (KeyboardType::None, true);
118 }
119 // Normal classification for internal and virtual keyboards
120 if !device.classes.contains(DeviceClass::External)
121 || device.classes.contains(DeviceClass::Virtual)
122 {
123 return if device.classes.contains(DeviceClass::AlphabeticKey) {
124 (KeyboardType::Alphabetic, true)
125 } else {
126 (KeyboardType::NonAlphabetic, true)
127 };
128 }
129 // Any composite device with multiple device classes should be categorized as non-alphabetic
130 // keyboard initially
131 if device.classes.contains(DeviceClass::Touch)
132 || device.classes.contains(DeviceClass::Cursor)
133 || device.classes.contains(DeviceClass::MultiTouch)
134 || device.classes.contains(DeviceClass::ExternalStylus)
135 || device.classes.contains(DeviceClass::Touchpad)
136 || device.classes.contains(DeviceClass::Dpad)
137 || device.classes.contains(DeviceClass::Gamepad)
138 || device.classes.contains(DeviceClass::Switch)
139 || device.classes.contains(DeviceClass::Joystick)
140 || device.classes.contains(DeviceClass::RotaryEncoder)
141 {
142 // If categorized as NonAlphabetic and no device class AlphabeticKey reported by the
143 // kernel, we no longer need to process key events to verify.
144 return (
145 KeyboardType::NonAlphabetic,
146 !device.classes.contains(DeviceClass::AlphabeticKey),
147 );
148 }
149 // Only devices with "Keyboard" and "AlphabeticKey" should be classified as full keyboard
150 if device.classes.contains(DeviceClass::AlphabeticKey) {
151 (KeyboardType::Alphabetic, true)
152 } else {
153 // If categorized as NonAlphabetic and no device class AlphabeticKey reported by the
154 // kernel, we no longer need to process key events to verify.
155 (KeyboardType::NonAlphabetic, true)
156 }
157 }
158
159 fn is_alphabetic_key(evdev_code: &i32) -> bool {
160 // Keyboard alphabetic row 1 (Q W E R T Y U I O P [ ])
161 (16..=27).contains(evdev_code)
162 // Keyboard alphabetic row 2 (A S D F G H J K L ; ' `)
163 || (30..=41).contains(evdev_code)
164 // Keyboard alphabetic row 3 (\ Z X C V B N M , . /)
165 || (43..=53).contains(evdev_code)
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use crate::input::{DeviceId, InputDevice, KeyboardType};
172 use crate::keyboard_classifier::KeyboardClassifier;
173 use crate::{DeviceClass, ModifierState, RustInputDeviceIdentifier};
174
175 static DEVICE_ID: DeviceId = DeviceId(1);
176 static KEY_A: i32 = 30;
177 static KEY_1: i32 = 2;
178
179 #[test]
180 fn classify_external_alphabetic_keyboard() {
181 let mut classifier = KeyboardClassifier::new();
182 classifier.notify_keyboard_changed(create_device(
183 DeviceClass::Keyboard | DeviceClass::AlphabeticKey | DeviceClass::External,
184 ));
185 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::Alphabetic);
186 assert!(classifier.is_finalized(DEVICE_ID));
187 }
188
189 #[test]
190 fn classify_external_non_alphabetic_keyboard() {
191 let mut classifier = KeyboardClassifier::new();
192 classifier
193 .notify_keyboard_changed(create_device(DeviceClass::Keyboard | DeviceClass::External));
194 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
195 assert!(classifier.is_finalized(DEVICE_ID));
196 }
197
198 #[test]
199 fn classify_mouse_pretending_as_keyboard() {
200 let mut classifier = KeyboardClassifier::new();
201 classifier.notify_keyboard_changed(create_device(
202 DeviceClass::Keyboard
203 | DeviceClass::Cursor
204 | DeviceClass::AlphabeticKey
205 | DeviceClass::External,
206 ));
207 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
208 assert!(!classifier.is_finalized(DEVICE_ID));
209 }
210
211 #[test]
212 fn classify_touchpad_pretending_as_keyboard() {
213 let mut classifier = KeyboardClassifier::new();
214 classifier.notify_keyboard_changed(create_device(
215 DeviceClass::Keyboard
216 | DeviceClass::Touchpad
217 | DeviceClass::AlphabeticKey
218 | DeviceClass::External,
219 ));
220 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
221 assert!(!classifier.is_finalized(DEVICE_ID));
222 }
223
224 #[test]
225 fn classify_stylus_pretending_as_keyboard() {
226 let mut classifier = KeyboardClassifier::new();
227 classifier.notify_keyboard_changed(create_device(
228 DeviceClass::Keyboard
229 | DeviceClass::ExternalStylus
230 | DeviceClass::AlphabeticKey
231 | DeviceClass::External,
232 ));
233 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
234 assert!(!classifier.is_finalized(DEVICE_ID));
235 }
236
237 #[test]
238 fn classify_dpad_pretending_as_keyboard() {
239 let mut classifier = KeyboardClassifier::new();
240 classifier.notify_keyboard_changed(create_device(
241 DeviceClass::Keyboard
242 | DeviceClass::Dpad
243 | DeviceClass::AlphabeticKey
244 | DeviceClass::External,
245 ));
246 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
247 assert!(!classifier.is_finalized(DEVICE_ID));
248 }
249
250 #[test]
251 fn classify_joystick_pretending_as_keyboard() {
252 let mut classifier = KeyboardClassifier::new();
253 classifier.notify_keyboard_changed(create_device(
254 DeviceClass::Keyboard
255 | DeviceClass::Joystick
256 | DeviceClass::AlphabeticKey
257 | DeviceClass::External,
258 ));
259 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
260 assert!(!classifier.is_finalized(DEVICE_ID));
261 }
262
263 #[test]
264 fn classify_gamepad_pretending_as_keyboard() {
265 let mut classifier = KeyboardClassifier::new();
266 classifier.notify_keyboard_changed(create_device(
267 DeviceClass::Keyboard
268 | DeviceClass::Gamepad
269 | DeviceClass::AlphabeticKey
270 | DeviceClass::External,
271 ));
272 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
273 assert!(!classifier.is_finalized(DEVICE_ID));
274 }
275
276 #[test]
277 fn reclassify_keyboard_on_alphabetic_key_event() {
278 let mut classifier = KeyboardClassifier::new();
279 classifier.notify_keyboard_changed(create_device(
280 DeviceClass::Keyboard
281 | DeviceClass::Dpad
282 | DeviceClass::AlphabeticKey
283 | DeviceClass::External,
284 ));
285 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
286 assert!(!classifier.is_finalized(DEVICE_ID));
287
288 // on alphabetic key event
289 classifier.process_key(DEVICE_ID, KEY_A, ModifierState::None);
290 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::Alphabetic);
291 assert!(classifier.is_finalized(DEVICE_ID));
292 }
293
294 #[test]
295 fn dont_reclassify_keyboard_on_non_alphabetic_key_event() {
296 let mut classifier = KeyboardClassifier::new();
297 classifier.notify_keyboard_changed(create_device(
298 DeviceClass::Keyboard
299 | DeviceClass::Dpad
300 | DeviceClass::AlphabeticKey
301 | DeviceClass::External,
302 ));
303 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
304 assert!(!classifier.is_finalized(DEVICE_ID));
305
306 // on number key event
307 classifier.process_key(DEVICE_ID, KEY_1, ModifierState::None);
308 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
309 assert!(!classifier.is_finalized(DEVICE_ID));
310 }
311
312 #[test]
313 fn dont_reclassify_keyboard_on_alphabetic_key_event_with_modifiers() {
314 let mut classifier = KeyboardClassifier::new();
315 classifier.notify_keyboard_changed(create_device(
316 DeviceClass::Keyboard
317 | DeviceClass::Dpad
318 | DeviceClass::AlphabeticKey
319 | DeviceClass::External,
320 ));
321 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
322 assert!(!classifier.is_finalized(DEVICE_ID));
323
324 classifier.process_key(DEVICE_ID, KEY_A, ModifierState::CtrlOn);
325 assert_eq!(classifier.get_keyboard_type(DEVICE_ID), KeyboardType::NonAlphabetic);
326 assert!(!classifier.is_finalized(DEVICE_ID));
327 }
328
329 fn create_device(classes: DeviceClass) -> InputDevice {
330 InputDevice {
331 device_id: DEVICE_ID,
332 identifier: RustInputDeviceIdentifier {
333 name: "test_device".to_string(),
334 location: "location".to_string(),
335 unique_id: "unique_id".to_string(),
336 bus: 123,
337 vendor: 234,
338 product: 345,
339 version: 567,
340 descriptor: "descriptor".to_string(),
341 },
342 classes,
343 }
Vaibhav Devmurarie58ffb92024-05-22 17:38:25 +0000344 }
345}