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