Vaibhav Devmurari | e58ffb9 | 2024-05-22 17:38:25 +0000 | [diff] [blame] | 1 | /* |
| 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 Devmurari | 2e73b2a | 2024-06-07 17:45:19 +0000 | [diff] [blame] | 20 | //! |
| 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 Devmurari | e58ffb9 | 2024-05-22 17:38:25 +0000 | [diff] [blame] | 36 | |
| 37 | use crate::input::{DeviceId, InputDevice, KeyboardType}; |
Vaibhav Devmurari | a8359fc | 2024-06-10 20:01:25 +0000 | [diff] [blame^] | 38 | use crate::keyboard_classification_config::CLASSIFIED_DEVICES; |
Vaibhav Devmurari | 2e73b2a | 2024-06-07 17:45:19 +0000 | [diff] [blame] | 39 | use crate::{DeviceClass, ModifierState}; |
| 40 | use std::collections::HashMap; |
Vaibhav Devmurari | e58ffb9 | 2024-05-22 17:38:25 +0000 | [diff] [blame] | 41 | |
| 42 | /// The KeyboardClassifier is used to classify a keyboard device into non-keyboard, alphabetic |
| 43 | /// keyboard or non-alphabetic keyboard |
| 44 | #[derive(Default)] |
Vaibhav Devmurari | 2e73b2a | 2024-06-07 17:45:19 +0000 | [diff] [blame] | 45 | pub struct KeyboardClassifier { |
| 46 | device_map: HashMap<DeviceId, KeyboardInfo>, |
| 47 | } |
| 48 | |
| 49 | struct KeyboardInfo { |
| 50 | _device: InputDevice, |
| 51 | keyboard_type: KeyboardType, |
| 52 | is_finalized: bool, |
| 53 | } |
Vaibhav Devmurari | e58ffb9 | 2024-05-22 17:38:25 +0000 | [diff] [blame] | 54 | |
| 55 | impl KeyboardClassifier { |
| 56 | /// Create a new KeyboardClassifier |
| 57 | pub fn new() -> Self { |
| 58 | Default::default() |
| 59 | } |
| 60 | |
| 61 | /// Adds keyboard to KeyboardClassifier |
Vaibhav Devmurari | 2e73b2a | 2024-06-07 17:45:19 +0000 | [diff] [blame] | 62 | 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 Devmurari | e58ffb9 | 2024-05-22 17:38:25 +0000 | [diff] [blame] | 68 | } |
| 69 | |
| 70 | /// Get keyboard type for a tracked keyboard in KeyboardClassifier |
Vaibhav Devmurari | 2e73b2a | 2024-06-07 17:45:19 +0000 | [diff] [blame] | 71 | 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 Devmurari | e58ffb9 | 2024-05-22 17:38:25 +0000 | [diff] [blame] | 77 | } |
| 78 | |
| 79 | /// Tells if keyboard type classification is finalized. Once finalized the classification can't |
| 80 | /// change until device is reconnected again. |
Vaibhav Devmurari | 2e73b2a | 2024-06-07 17:45:19 +0000 | [diff] [blame] | 81 | /// |
| 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 Devmurari | e58ffb9 | 2024-05-22 17:38:25 +0000 | [diff] [blame] | 90 | } |
| 91 | |
| 92 | /// Process a key event and change keyboard type if required. |
Vaibhav Devmurari | 2e73b2a | 2024-06-07 17:45:19 +0000 | [diff] [blame] | 93 | /// - 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 Devmurari | e58ffb9 | 2024-05-22 17:38:25 +0000 | [diff] [blame] | 95 | pub fn process_key( |
| 96 | &mut self, |
Vaibhav Devmurari | 2e73b2a | 2024-06-07 17:45:19 +0000 | [diff] [blame] | 97 | device_id: DeviceId, |
| 98 | evdev_code: i32, |
| 99 | modifier_state: ModifierState, |
Vaibhav Devmurari | e58ffb9 | 2024-05-22 17:38:25 +0000 | [diff] [blame] | 100 | ) { |
Vaibhav Devmurari | 2e73b2a | 2024-06-07 17:45:19 +0000 | [diff] [blame] | 101 | 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 Devmurari | a8359fc | 2024-06-10 20:01:25 +0000 | [diff] [blame^] | 130 | |
| 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 Devmurari | 2e73b2a | 2024-06-07 17:45:19 +0000 | [diff] [blame] | 138 | // 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)] |
| 179 | mod tests { |
| 180 | use crate::input::{DeviceId, InputDevice, KeyboardType}; |
Vaibhav Devmurari | a8359fc | 2024-06-10 20:01:25 +0000 | [diff] [blame^] | 181 | use crate::keyboard_classification_config::CLASSIFIED_DEVICES; |
Vaibhav Devmurari | 2e73b2a | 2024-06-07 17:45:19 +0000 | [diff] [blame] | 182 | 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 Devmurari | a8359fc | 2024-06-10 20:01:25 +0000 | [diff] [blame^] | 339 | #[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 Devmurari | 2e73b2a | 2024-06-07 17:45:19 +0000 | [diff] [blame] | 350 | 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 Devmurari | e58ffb9 | 2024-05-22 17:38:25 +0000 | [diff] [blame] | 365 | } |
Vaibhav Devmurari | a8359fc | 2024-06-10 20:01:25 +0000 | [diff] [blame^] | 366 | |
| 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 Devmurari | e58ffb9 | 2024-05-22 17:38:25 +0000 | [diff] [blame] | 383 | } |