diff options
author | 2023-12-15 04:39:01 +0000 | |
---|---|---|
committer | 2023-12-15 04:39:01 +0000 | |
commit | f5e1fa26b7b78489ec05eecdf942c7c8608df935 (patch) | |
tree | 7e34d8c3b370e71785f12e13bf2d9bd42822a4f8 | |
parent | bae8b023d7a2a775470058b49ed540b0f9ddf06d (diff) | |
parent | 93144689a880e037bf9447d579803f836ec16488 (diff) |
Merge "Add sticky keys input filter" into main
-rw-r--r-- | services/inputflinger/Android.bp | 1 | ||||
-rw-r--r-- | services/inputflinger/InputFilter.cpp | 41 | ||||
-rw-r--r-- | services/inputflinger/InputFilter.h | 5 | ||||
-rw-r--r-- | services/inputflinger/InputFilterCallbacks.cpp | 60 | ||||
-rw-r--r-- | services/inputflinger/InputFilterCallbacks.h | 55 | ||||
-rw-r--r-- | services/inputflinger/aidl/com/android/server/inputflinger/IInputFilter.aidl | 3 | ||||
-rw-r--r-- | services/inputflinger/aidl/com/android/server/inputflinger/InputFilterConfiguration.aidl | 2 | ||||
-rw-r--r-- | services/inputflinger/rust/input_filter.rs | 161 | ||||
-rw-r--r-- | services/inputflinger/rust/lib.rs | 1 | ||||
-rw-r--r-- | services/inputflinger/rust/sticky_keys_filter.rs | 515 |
10 files changed, 786 insertions, 58 deletions
diff --git a/services/inputflinger/Android.bp b/services/inputflinger/Android.bp index 45c9b5cf0b..69f42bc800 100644 --- a/services/inputflinger/Android.bp +++ b/services/inputflinger/Android.bp @@ -77,6 +77,7 @@ filegroup { "InputCommonConverter.cpp", "InputDeviceMetricsCollector.cpp", "InputFilter.cpp", + "InputFilterCallbacks.cpp", "InputProcessor.cpp", "PointerChoreographer.cpp", "PreferStylusOverTouchBlocker.cpp", diff --git a/services/inputflinger/InputFilter.cpp b/services/inputflinger/InputFilter.cpp index 9c4a3eb274..5d87d34adb 100644 --- a/services/inputflinger/InputFilter.cpp +++ b/services/inputflinger/InputFilter.cpp @@ -44,31 +44,9 @@ AidlKeyEvent notifyKeyArgsToKeyEvent(const NotifyKeyArgs& args) { return event; } -NotifyKeyArgs keyEventToNotifyKeyArgs(const AidlKeyEvent& event) { - return NotifyKeyArgs(event.id, event.eventTime, event.readTime, event.deviceId, - static_cast<uint32_t>(event.source), event.displayId, event.policyFlags, - static_cast<int32_t>(event.action), event.flags, event.keyCode, - event.scanCode, event.metaState, event.downTime); -} - -namespace { - -class RustCallbacks : public IInputFilter::BnInputFilterCallbacks { -public: - RustCallbacks(InputListenerInterface& nextListener) : mNextListener(nextListener) {} - ndk::ScopedAStatus sendKeyEvent(const AidlKeyEvent& event) override { - mNextListener.notifyKey(keyEventToNotifyKeyArgs(event)); - return ndk::ScopedAStatus::ok(); - } - -private: - InputListenerInterface& mNextListener; -}; - -} // namespace - InputFilter::InputFilter(InputListenerInterface& listener, IInputFlingerRust& rust) - : mNextListener(listener), mCallbacks(ndk::SharedRefBase::make<RustCallbacks>(listener)) { + : mNextListener(listener), + mCallbacks(ndk::SharedRefBase::make<InputFilterCallbacks>(listener)) { LOG_ALWAYS_FATAL_IF(!rust.createInputFilter(mCallbacks, &mInputFilterRust).isOk()); LOG_ALWAYS_FATAL_IF(!mInputFilterRust); } @@ -92,11 +70,11 @@ void InputFilter::notifyConfigurationChanged(const NotifyConfigurationChangedArg } void InputFilter::notifyKey(const NotifyKeyArgs& args) { - if (!isFilterEnabled()) { - mNextListener.notifyKey(args); + if (isFilterEnabled()) { + LOG_ALWAYS_FATAL_IF(!mInputFilterRust->notifyKey(notifyKeyArgsToKeyEvent(args)).isOk()); return; } - LOG_ALWAYS_FATAL_IF(!mInputFilterRust->notifyKey(notifyKeyArgsToKeyEvent(args)).isOk()); + mNextListener.notifyKey(args); } void InputFilter::notifyMotion(const NotifyMotionArgs& args) { @@ -138,6 +116,15 @@ void InputFilter::setAccessibilityBounceKeysThreshold(nsecs_t threshold) { } } +void InputFilter::setAccessibilityStickyKeysEnabled(bool enabled) { + std::scoped_lock _l(mLock); + + if (mConfig.stickyKeysEnabled != enabled) { + mConfig.stickyKeysEnabled = enabled; + LOG_ALWAYS_FATAL_IF(!mInputFilterRust->notifyConfigurationChanged(mConfig).isOk()); + } +} + void InputFilter::dump(std::string& dump) { dump += "InputFilter:\n"; } diff --git a/services/inputflinger/InputFilter.h b/services/inputflinger/InputFilter.h index 06f7d0e601..9fa7a8758f 100644 --- a/services/inputflinger/InputFilter.h +++ b/services/inputflinger/InputFilter.h @@ -18,6 +18,7 @@ #include <aidl/com/android/server/inputflinger/IInputFlingerRust.h> #include <utils/Mutex.h> +#include "InputFilterCallbacks.h" #include "InputListener.h" #include "NotifyArgs.h" @@ -33,6 +34,7 @@ public: */ virtual void dump(std::string& dump) = 0; virtual void setAccessibilityBounceKeysThreshold(nsecs_t threshold) = 0; + virtual void setAccessibilityStickyKeysEnabled(bool enabled) = 0; }; class InputFilter : public InputFilterInterface { @@ -56,11 +58,12 @@ public: void notifyDeviceReset(const NotifyDeviceResetArgs& args) override; void notifyPointerCaptureChanged(const NotifyPointerCaptureChangedArgs& args) override; void setAccessibilityBounceKeysThreshold(nsecs_t threshold) override; + void setAccessibilityStickyKeysEnabled(bool enabled) override; void dump(std::string& dump) override; private: InputListenerInterface& mNextListener; - std::shared_ptr<IInputFilterCallbacks> mCallbacks; + std::shared_ptr<InputFilterCallbacks> mCallbacks; std::shared_ptr<IInputFilter> mInputFilterRust; mutable std::mutex mLock; InputFilterConfiguration mConfig GUARDED_BY(mLock); diff --git a/services/inputflinger/InputFilterCallbacks.cpp b/services/inputflinger/InputFilterCallbacks.cpp new file mode 100644 index 0000000000..8c8f5e8f71 --- /dev/null +++ b/services/inputflinger/InputFilterCallbacks.cpp @@ -0,0 +1,60 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "InputFilterCallbacks" + +#include "InputFilterCallbacks.h" + +namespace android { + +using AidlKeyEvent = aidl::com::android::server::inputflinger::KeyEvent; + +NotifyKeyArgs keyEventToNotifyKeyArgs(const AidlKeyEvent& event) { + return NotifyKeyArgs(event.id, event.eventTime, event.readTime, event.deviceId, + static_cast<uint32_t>(event.source), event.displayId, event.policyFlags, + static_cast<int32_t>(event.action), event.flags, event.keyCode, + event.scanCode, event.metaState, event.downTime); +} + +InputFilterCallbacks::InputFilterCallbacks(InputListenerInterface& listener) + : mNextListener(listener) {} + +ndk::ScopedAStatus InputFilterCallbacks::sendKeyEvent(const AidlKeyEvent& event) { + mNextListener.notifyKey(keyEventToNotifyKeyArgs(event)); + return ndk::ScopedAStatus::ok(); +} + +ndk::ScopedAStatus InputFilterCallbacks::onModifierStateChanged(int32_t modifierState, + int32_t lockedModifierState) { + std::scoped_lock _l(mLock); + mStickyModifierState.modifierState = modifierState; + mStickyModifierState.lockedModifierState = lockedModifierState; + ALOGI("Sticky keys modifier state changed: modifierState=%d, lockedModifierState=%d", + modifierState, lockedModifierState); + return ndk::ScopedAStatus::ok(); +} + +uint32_t InputFilterCallbacks::getModifierState() { + std::scoped_lock _l(mLock); + return mStickyModifierState.modifierState; +} + +uint32_t InputFilterCallbacks::getLockedModifierState() { + std::scoped_lock _l(mLock); + return mStickyModifierState.lockedModifierState; +} + +} // namespace android diff --git a/services/inputflinger/InputFilterCallbacks.h b/services/inputflinger/InputFilterCallbacks.h new file mode 100644 index 0000000000..c0a80fb6dc --- /dev/null +++ b/services/inputflinger/InputFilterCallbacks.h @@ -0,0 +1,55 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <aidl/com/android/server/inputflinger/IInputFlingerRust.h> +#include <android/binder_auto_utils.h> +#include <utils/Mutex.h> +#include <mutex> +#include "InputListener.h" +#include "NotifyArgs.h" + +/** + * The C++ component of InputFilter designed as a wrapper around the rust callback implementation. + */ +namespace android { + +using IInputFilter = aidl::com::android::server::inputflinger::IInputFilter; +using AidlKeyEvent = aidl::com::android::server::inputflinger::KeyEvent; + +class InputFilterCallbacks : public IInputFilter::BnInputFilterCallbacks { +public: + explicit InputFilterCallbacks(InputListenerInterface& listener); + ~InputFilterCallbacks() override = default; + + uint32_t getModifierState(); + uint32_t getLockedModifierState(); + +private: + InputListenerInterface& mNextListener; + mutable std::mutex mLock; + struct StickyModifierState { + uint32_t modifierState; + uint32_t lockedModifierState; + } mStickyModifierState GUARDED_BY(mLock); + + ndk::ScopedAStatus sendKeyEvent(const AidlKeyEvent& event) override; + ndk::ScopedAStatus onModifierStateChanged(int32_t modifierState, + int32_t lockedModifierState) override; +}; + +} // namespace android
\ No newline at end of file diff --git a/services/inputflinger/aidl/com/android/server/inputflinger/IInputFilter.aidl b/services/inputflinger/aidl/com/android/server/inputflinger/IInputFilter.aidl index 14b41cd00c..2921d30b22 100644 --- a/services/inputflinger/aidl/com/android/server/inputflinger/IInputFilter.aidl +++ b/services/inputflinger/aidl/com/android/server/inputflinger/IInputFilter.aidl @@ -33,6 +33,9 @@ interface IInputFilter { interface IInputFilterCallbacks { /** Sends back a filtered key event */ void sendKeyEvent(in KeyEvent event); + + /** Sends back modifier state */ + void onModifierStateChanged(int modifierState, int lockedModifierState); } /** Returns if InputFilter is enabled */ diff --git a/services/inputflinger/aidl/com/android/server/inputflinger/InputFilterConfiguration.aidl b/services/inputflinger/aidl/com/android/server/inputflinger/InputFilterConfiguration.aidl index 3b2e88ba24..38b161203b 100644 --- a/services/inputflinger/aidl/com/android/server/inputflinger/InputFilterConfiguration.aidl +++ b/services/inputflinger/aidl/com/android/server/inputflinger/InputFilterConfiguration.aidl @@ -22,4 +22,6 @@ package com.android.server.inputflinger; parcelable InputFilterConfiguration { // Threshold value for Bounce keys filter (check bounce_keys_filter.rs) long bounceKeysThresholdNs; + // If sticky keys filter is enabled + boolean stickyKeysEnabled; }
\ No newline at end of file diff --git a/services/inputflinger/rust/input_filter.rs b/services/inputflinger/rust/input_filter.rs index 340ff8e296..e94a71fbf8 100644 --- a/services/inputflinger/rust/input_filter.rs +++ b/services/inputflinger/rust/input_filter.rs @@ -27,6 +27,7 @@ use com_android_server_inputflinger::aidl::com::android::server::inputflinger::{ }; use crate::bounce_keys_filter::BounceKeysFilter; +use crate::sticky_keys_filter::StickyKeysFilter; use log::{error, info}; use std::sync::{Arc, Mutex, RwLock}; @@ -91,6 +92,14 @@ impl IInputFilter for InputFilter { let mut state = self.state.lock().unwrap(); let mut first_filter: Box<dyn Filter + Send + Sync> = Box::new(BaseFilter::new(self.callbacks.clone())); + if config.stickyKeysEnabled { + first_filter = Box::new(StickyKeysFilter::new( + first_filter, + ModifierStateListener::new(self.callbacks.clone()), + )); + state.enabled = true; + info!("Sticky keys filter is installed"); + } if config.bounceKeysThresholdNs > 0 { first_filter = Box::new(BounceKeysFilter::new(first_filter, config.bounceKeysThresholdNs)); @@ -125,34 +134,43 @@ impl Filter for BaseFilter { } } +pub struct ModifierStateListener { + callbacks: Arc<RwLock<Strong<dyn IInputFilterCallbacks>>>, +} + +impl ModifierStateListener { + /// Create a new InputFilter instance. + pub fn new(callbacks: Arc<RwLock<Strong<dyn IInputFilterCallbacks>>>) -> ModifierStateListener { + Self { callbacks } + } + + pub fn modifier_state_changed(&self, modifier_state: u32, locked_modifier_state: u32) { + let _ = self + .callbacks + .read() + .unwrap() + .onModifierStateChanged(modifier_state as i32, locked_modifier_state as i32); + } +} + #[cfg(test)] mod tests { - use crate::input_filter::{test_filter::TestFilter, Filter, InputFilter}; + use crate::input_filter::{ + test_callbacks::TestCallbacks, test_filter::TestFilter, InputFilter, + }; use android_hardware_input_common::aidl::android::hardware::input::common::Source::Source; - use binder::{Interface, Strong}; + use binder::Strong; use com_android_server_inputflinger::aidl::com::android::server::inputflinger::{ DeviceInfo::DeviceInfo, IInputFilter::IInputFilter, - IInputFilter::IInputFilterCallbacks::IInputFilterCallbacks, InputFilterConfiguration::InputFilterConfiguration, KeyEvent::KeyEvent, KeyEventAction::KeyEventAction, }; use std::sync::{Arc, RwLock}; - struct FakeCallbacks {} - - impl Interface for FakeCallbacks {} - - impl IInputFilterCallbacks for FakeCallbacks { - fn sendKeyEvent(&self, _event: &KeyEvent) -> binder::Result<()> { - Result::Ok(()) - } - } - #[test] fn test_not_enabled_with_default_filter() { - let fake_callbacks: Strong<dyn IInputFilterCallbacks> = - Strong::new(Box::new(FakeCallbacks {})); - let input_filter = InputFilter::new(fake_callbacks); + let test_callbacks = TestCallbacks::new(); + let input_filter = InputFilter::new(Strong::new(Box::new(test_callbacks))); let result = input_filter.isEnabled(); assert!(result.is_ok()); assert!(!result.unwrap()); @@ -160,17 +178,21 @@ mod tests { #[test] fn test_notify_key_with_no_filters() { - let fake_callbacks: Strong<dyn IInputFilterCallbacks> = - Strong::new(Box::new(FakeCallbacks {})); - let input_filter = InputFilter::new(fake_callbacks); + let test_callbacks = TestCallbacks::new(); + let input_filter = InputFilter::new(Strong::new(Box::new(test_callbacks.clone()))); let event = create_key_event(); assert!(input_filter.notifyKey(&event).is_ok()); + assert_eq!(test_callbacks.last_event().unwrap(), event); } #[test] fn test_notify_key_with_filter() { let test_filter = TestFilter::new(); - let input_filter = create_input_filter(Box::new(test_filter.clone())); + let test_callbacks = TestCallbacks::new(); + let input_filter = InputFilter::create_input_filter( + Box::new(test_filter.clone()), + Arc::new(RwLock::new(Strong::new(Box::new(test_callbacks)))), + ); let event = create_key_event(); assert!(input_filter.notifyKey(&event).is_ok()); assert_eq!(test_filter.last_event().unwrap(), event); @@ -179,7 +201,11 @@ mod tests { #[test] fn test_notify_devices_changed() { let test_filter = TestFilter::new(); - let input_filter = create_input_filter(Box::new(test_filter.clone())); + let test_callbacks = TestCallbacks::new(); + let input_filter = InputFilter::create_input_filter( + Box::new(test_filter.clone()), + Arc::new(RwLock::new(Strong::new(Box::new(test_callbacks)))), + ); assert!(input_filter .notifyInputDevicesChanged(&[DeviceInfo { deviceId: 0, external: true }]) .is_ok()); @@ -188,21 +214,30 @@ mod tests { #[test] fn test_notify_configuration_changed_enabled_bounce_keys() { - let fake_callbacks: Strong<dyn IInputFilterCallbacks> = - Strong::new(Box::new(FakeCallbacks {})); - let input_filter = InputFilter::new(fake_callbacks); - let result = input_filter - .notifyConfigurationChanged(&InputFilterConfiguration { bounceKeysThresholdNs: 100 }); + let test_callbacks = TestCallbacks::new(); + let input_filter = InputFilter::new(Strong::new(Box::new(test_callbacks))); + let result = input_filter.notifyConfigurationChanged(&InputFilterConfiguration { + bounceKeysThresholdNs: 100, + stickyKeysEnabled: false, + }); assert!(result.is_ok()); let result = input_filter.isEnabled(); assert!(result.is_ok()); assert!(result.unwrap()); } - fn create_input_filter(filter: Box<dyn Filter + Send + Sync>) -> InputFilter { - let fake_callbacks: Strong<dyn IInputFilterCallbacks> = - Strong::new(Box::new(FakeCallbacks {})); - InputFilter::create_input_filter(filter, Arc::new(RwLock::new(fake_callbacks))) + #[test] + fn test_notify_configuration_changed_enabled_sticky_keys() { + let test_callbacks = TestCallbacks::new(); + let input_filter = InputFilter::new(Strong::new(Box::new(test_callbacks))); + let result = input_filter.notifyConfigurationChanged(&InputFilterConfiguration { + bounceKeysThresholdNs: 0, + stickyKeysEnabled: true, + }); + assert!(result.is_ok()); + let result = input_filter.isEnabled(); + assert!(result.is_ok()); + assert!(result.unwrap()); } fn create_key_event() -> KeyEvent { @@ -272,3 +307,69 @@ pub mod test_filter { } } } + +#[cfg(test)] +pub mod test_callbacks { + use binder::Interface; + use com_android_server_inputflinger::aidl::com::android::server::inputflinger::{ + IInputFilter::IInputFilterCallbacks::IInputFilterCallbacks, KeyEvent::KeyEvent, + }; + use std::sync::{Arc, RwLock, RwLockWriteGuard}; + + #[derive(Default)] + struct TestCallbacksInner { + last_modifier_state: u32, + last_locked_modifier_state: u32, + last_event: Option<KeyEvent>, + } + + #[derive(Default, Clone)] + pub struct TestCallbacks(Arc<RwLock<TestCallbacksInner>>); + + impl Interface for TestCallbacks {} + + impl TestCallbacks { + pub fn new() -> Self { + Default::default() + } + + fn inner(&self) -> RwLockWriteGuard<'_, TestCallbacksInner> { + self.0.write().unwrap() + } + + pub fn last_event(&self) -> Option<KeyEvent> { + self.0.read().unwrap().last_event + } + + pub fn clear(&mut self) { + self.inner().last_event = None; + self.inner().last_modifier_state = 0; + self.inner().last_locked_modifier_state = 0; + } + + pub fn get_last_modifier_state(&self) -> u32 { + self.0.read().unwrap().last_modifier_state + } + + pub fn get_last_locked_modifier_state(&self) -> u32 { + self.0.read().unwrap().last_locked_modifier_state + } + } + + impl IInputFilterCallbacks for TestCallbacks { + fn sendKeyEvent(&self, event: &KeyEvent) -> binder::Result<()> { + self.inner().last_event = Some(*event); + Result::Ok(()) + } + + fn onModifierStateChanged( + &self, + modifier_state: i32, + locked_modifier_state: i32, + ) -> std::result::Result<(), binder::Status> { + self.inner().last_modifier_state = modifier_state as u32; + self.inner().last_locked_modifier_state = locked_modifier_state as u32; + Result::Ok(()) + } + } +} diff --git a/services/inputflinger/rust/lib.rs b/services/inputflinger/rust/lib.rs index 68cd4800fe..fa16898835 100644 --- a/services/inputflinger/rust/lib.rs +++ b/services/inputflinger/rust/lib.rs @@ -21,6 +21,7 @@ mod bounce_keys_filter; mod input_filter; +mod sticky_keys_filter; use crate::input_filter::InputFilter; use binder::{ diff --git a/services/inputflinger/rust/sticky_keys_filter.rs b/services/inputflinger/rust/sticky_keys_filter.rs new file mode 100644 index 0000000000..da581b82bf --- /dev/null +++ b/services/inputflinger/rust/sticky_keys_filter.rs @@ -0,0 +1,515 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Sticky keys input filter implementation. +//! Sticky keys is an accessibility feature that assists users who have physical disabilities or +//! helps users reduce repetitive strain injury. It serializes keystrokes instead of pressing +//! multiple keys at a time, allowing the user to press and release a modifier key, such as Shift, +//! Ctrl, Alt, or any other modifier key, and have it remain active until any other key is pressed. +use crate::input_filter::{Filter, ModifierStateListener}; +use com_android_server_inputflinger::aidl::com::android::server::inputflinger::{ + DeviceInfo::DeviceInfo, KeyEvent::KeyEvent, KeyEventAction::KeyEventAction, +}; +use std::collections::HashSet; + +// Modifier keycodes: values are from /frameworks/native/include/android/keycodes.h +const KEYCODE_ALT_LEFT: i32 = 57; +const KEYCODE_ALT_RIGHT: i32 = 58; +const KEYCODE_SHIFT_LEFT: i32 = 59; +const KEYCODE_SHIFT_RIGHT: i32 = 60; +const KEYCODE_SYM: i32 = 63; +const KEYCODE_CTRL_LEFT: i32 = 113; +const KEYCODE_CTRL_RIGHT: i32 = 114; +const KEYCODE_CAPS_LOCK: i32 = 115; +const KEYCODE_SCROLL_LOCK: i32 = 116; +const KEYCODE_META_LEFT: i32 = 117; +const KEYCODE_META_RIGHT: i32 = 118; +const KEYCODE_FUNCTION: i32 = 119; +const KEYCODE_NUM_LOCK: i32 = 143; + +// Modifier states: values are from /frameworks/native/include/android/input.h +const META_ALT_ON: u32 = 0x02; +const META_ALT_LEFT_ON: u32 = 0x10; +const META_ALT_RIGHT_ON: u32 = 0x20; +const META_SHIFT_ON: u32 = 0x01; +const META_SHIFT_LEFT_ON: u32 = 0x40; +const META_SHIFT_RIGHT_ON: u32 = 0x80; +const META_CTRL_ON: u32 = 0x1000; +const META_CTRL_LEFT_ON: u32 = 0x2000; +const META_CTRL_RIGHT_ON: u32 = 0x4000; +const META_META_ON: u32 = 0x10000; +const META_META_LEFT_ON: u32 = 0x20000; +const META_META_RIGHT_ON: u32 = 0x40000; + +pub struct StickyKeysFilter { + next: Box<dyn Filter + Send + Sync>, + listener: ModifierStateListener, + /// Tracking devices that contributed to the modifier state. + contributing_devices: HashSet<i32>, + /// State describing the current enabled modifiers. This contain both locked and non-locked + /// modifier state bits. + modifier_state: u32, + /// State describing the current locked modifiers. These modifiers will not be cleared on a + /// non-modifier key press. They will be cleared only if the locked modifier key is pressed + /// again. + locked_modifier_state: u32, +} + +impl StickyKeysFilter { + /// Create a new StickyKeysFilter instance. + pub fn new( + next: Box<dyn Filter + Send + Sync>, + listener: ModifierStateListener, + ) -> StickyKeysFilter { + Self { + next, + listener, + contributing_devices: HashSet::new(), + modifier_state: 0, + locked_modifier_state: 0, + } + } +} + +impl Filter for StickyKeysFilter { + fn notify_key(&mut self, event: &KeyEvent) { + let up = event.action == KeyEventAction::UP; + let mut modifier_state = self.modifier_state; + let mut locked_modifier_state = self.locked_modifier_state; + if !is_ephemeral_modifier_key(event.keyCode) { + // If non-ephemeral modifier key (i.e. non-modifier keys + toggle modifier keys like + // CAPS_LOCK, NUM_LOCK etc.), don't block key and pass in the sticky modifier state with + // the KeyEvent. + let old_modifier_state = event.metaState as u32; + let mut new_event = *event; + // Send the current modifier state with the key event before clearing non-locked + // modifier state + new_event.metaState = + (clear_ephemeral_modifier_state(old_modifier_state) | modifier_state) as i32; + self.next.notify_key(&new_event); + if up && !is_modifier_key(event.keyCode) { + modifier_state = + clear_ephemeral_modifier_state(modifier_state) | locked_modifier_state; + } + } else if up { + // Update contributing devices to track keyboards + self.contributing_devices.insert(event.deviceId); + // If ephemeral modifier key, capture the key and update the sticky modifier states + let modifier_key_mask = get_ephemeral_modifier_key_mask(event.keyCode); + let symmetrical_modifier_key_mask = get_symmetrical_modifier_key_mask(event.keyCode); + if locked_modifier_state & modifier_key_mask != 0 { + locked_modifier_state &= !symmetrical_modifier_key_mask; + modifier_state &= !symmetrical_modifier_key_mask; + } else if modifier_key_mask & modifier_state != 0 { + locked_modifier_state |= modifier_key_mask; + modifier_state = + (modifier_state & !symmetrical_modifier_key_mask) | modifier_key_mask; + } else { + modifier_state |= modifier_key_mask; + } + } + if self.modifier_state != modifier_state + || self.locked_modifier_state != locked_modifier_state + { + self.modifier_state = modifier_state; + self.locked_modifier_state = locked_modifier_state; + self.listener.modifier_state_changed(modifier_state, locked_modifier_state); + } + } + + fn notify_devices_changed(&mut self, device_infos: &[DeviceInfo]) { + // Clear state if all contributing devices removed + self.contributing_devices.retain(|id| device_infos.iter().any(|x| *id == x.deviceId)); + if self.contributing_devices.is_empty() + && (self.modifier_state != 0 || self.locked_modifier_state != 0) + { + self.modifier_state = 0; + self.locked_modifier_state = 0; + self.listener.modifier_state_changed(0, 0); + } + self.next.notify_devices_changed(device_infos); + } +} + +fn is_modifier_key(keycode: i32) -> bool { + matches!( + keycode, + KEYCODE_ALT_LEFT + | KEYCODE_ALT_RIGHT + | KEYCODE_SHIFT_LEFT + | KEYCODE_SHIFT_RIGHT + | KEYCODE_CTRL_LEFT + | KEYCODE_CTRL_RIGHT + | KEYCODE_META_LEFT + | KEYCODE_META_RIGHT + | KEYCODE_SYM + | KEYCODE_FUNCTION + | KEYCODE_CAPS_LOCK + | KEYCODE_NUM_LOCK + | KEYCODE_SCROLL_LOCK + ) +} + +fn is_ephemeral_modifier_key(keycode: i32) -> bool { + matches!( + keycode, + KEYCODE_ALT_LEFT + | KEYCODE_ALT_RIGHT + | KEYCODE_SHIFT_LEFT + | KEYCODE_SHIFT_RIGHT + | KEYCODE_CTRL_LEFT + | KEYCODE_CTRL_RIGHT + | KEYCODE_META_LEFT + | KEYCODE_META_RIGHT + ) +} + +fn get_ephemeral_modifier_key_mask(keycode: i32) -> u32 { + match keycode { + KEYCODE_ALT_LEFT => META_ALT_LEFT_ON | META_ALT_ON, + KEYCODE_ALT_RIGHT => META_ALT_RIGHT_ON | META_ALT_ON, + KEYCODE_SHIFT_LEFT => META_SHIFT_LEFT_ON | META_SHIFT_ON, + KEYCODE_SHIFT_RIGHT => META_SHIFT_RIGHT_ON | META_SHIFT_ON, + KEYCODE_CTRL_LEFT => META_CTRL_LEFT_ON | META_CTRL_ON, + KEYCODE_CTRL_RIGHT => META_CTRL_RIGHT_ON | META_CTRL_ON, + KEYCODE_META_LEFT => META_META_LEFT_ON | META_META_ON, + KEYCODE_META_RIGHT => META_META_RIGHT_ON | META_META_ON, + _ => 0, + } +} + +/// Modifier mask including both left and right versions of a modifier key. +fn get_symmetrical_modifier_key_mask(keycode: i32) -> u32 { + match keycode { + KEYCODE_ALT_LEFT | KEYCODE_ALT_RIGHT => META_ALT_LEFT_ON | META_ALT_RIGHT_ON | META_ALT_ON, + KEYCODE_SHIFT_LEFT | KEYCODE_SHIFT_RIGHT => { + META_SHIFT_LEFT_ON | META_SHIFT_RIGHT_ON | META_SHIFT_ON + } + KEYCODE_CTRL_LEFT | KEYCODE_CTRL_RIGHT => { + META_CTRL_LEFT_ON | META_CTRL_RIGHT_ON | META_CTRL_ON + } + KEYCODE_META_LEFT | KEYCODE_META_RIGHT => { + META_META_LEFT_ON | META_META_RIGHT_ON | META_META_ON + } + _ => 0, + } +} + +fn clear_ephemeral_modifier_state(modifier_state: u32) -> u32 { + modifier_state + & !(META_ALT_LEFT_ON + | META_ALT_RIGHT_ON + | META_ALT_ON + | META_SHIFT_LEFT_ON + | META_SHIFT_RIGHT_ON + | META_SHIFT_ON + | META_CTRL_LEFT_ON + | META_CTRL_RIGHT_ON + | META_CTRL_ON + | META_META_LEFT_ON + | META_META_RIGHT_ON + | META_META_ON) +} + +#[cfg(test)] +mod tests { + use crate::input_filter::{ + test_callbacks::TestCallbacks, test_filter::TestFilter, Filter, ModifierStateListener, + }; + use crate::sticky_keys_filter::{ + StickyKeysFilter, KEYCODE_ALT_LEFT, KEYCODE_ALT_RIGHT, KEYCODE_CAPS_LOCK, + KEYCODE_CTRL_LEFT, KEYCODE_CTRL_RIGHT, KEYCODE_FUNCTION, KEYCODE_META_LEFT, + KEYCODE_META_RIGHT, KEYCODE_NUM_LOCK, KEYCODE_SCROLL_LOCK, KEYCODE_SHIFT_LEFT, + KEYCODE_SHIFT_RIGHT, KEYCODE_SYM, META_ALT_LEFT_ON, META_ALT_ON, META_ALT_RIGHT_ON, + META_CTRL_LEFT_ON, META_CTRL_ON, META_CTRL_RIGHT_ON, META_META_LEFT_ON, META_META_ON, + META_META_RIGHT_ON, META_SHIFT_LEFT_ON, META_SHIFT_ON, META_SHIFT_RIGHT_ON, + }; + use android_hardware_input_common::aidl::android::hardware::input::common::Source::Source; + use binder::Strong; + use com_android_server_inputflinger::aidl::com::android::server::inputflinger::{ + DeviceInfo::DeviceInfo, IInputFilter::IInputFilterCallbacks::IInputFilterCallbacks, + KeyEvent::KeyEvent, KeyEventAction::KeyEventAction, + }; + use std::sync::{Arc, RwLock}; + + static DEVICE_ID: i32 = 1; + static KEY_A: i32 = 29; + static BASE_KEY_DOWN: KeyEvent = KeyEvent { + id: 1, + deviceId: DEVICE_ID, + downTime: 0, + readTime: 0, + eventTime: 0, + source: Source::KEYBOARD, + displayId: 0, + policyFlags: 0, + action: KeyEventAction::DOWN, + flags: 0, + keyCode: 0, + scanCode: 0, + metaState: 0, + }; + + static BASE_KEY_UP: KeyEvent = KeyEvent { action: KeyEventAction::UP, ..BASE_KEY_DOWN }; + + #[test] + fn test_notify_key_consumes_ephemeral_modifier_keys() { + let test_filter = TestFilter::new(); + let test_callbacks = TestCallbacks::new(); + let mut sticky_keys_filter = setup_filter( + Box::new(test_filter.clone()), + Arc::new(RwLock::new(Strong::new(Box::new(test_callbacks.clone())))), + ); + let key_codes = &[ + KEYCODE_ALT_LEFT, + KEYCODE_ALT_RIGHT, + KEYCODE_CTRL_LEFT, + KEYCODE_CTRL_RIGHT, + KEYCODE_SHIFT_LEFT, + KEYCODE_SHIFT_RIGHT, + KEYCODE_META_LEFT, + KEYCODE_META_RIGHT, + ]; + for key_code in key_codes.iter() { + sticky_keys_filter.notify_key(&KeyEvent { keyCode: *key_code, ..BASE_KEY_DOWN }); + assert!(test_filter.last_event().is_none()); + + sticky_keys_filter.notify_key(&KeyEvent { keyCode: *key_code, ..BASE_KEY_UP }); + assert!(test_filter.last_event().is_none()); + } + } + + #[test] + fn test_notify_key_passes_non_ephemeral_modifier_keys() { + let test_filter = TestFilter::new(); + let test_callbacks = TestCallbacks::new(); + let mut sticky_keys_filter = setup_filter( + Box::new(test_filter.clone()), + Arc::new(RwLock::new(Strong::new(Box::new(test_callbacks.clone())))), + ); + let key_codes = &[ + KEYCODE_CAPS_LOCK, + KEYCODE_NUM_LOCK, + KEYCODE_SCROLL_LOCK, + KEYCODE_FUNCTION, + KEYCODE_SYM, + ]; + for key_code in key_codes.iter() { + let event = KeyEvent { keyCode: *key_code, ..BASE_KEY_DOWN }; + sticky_keys_filter.notify_key(&event); + assert_eq!(test_filter.last_event().unwrap(), event); + let event = KeyEvent { keyCode: *key_code, ..BASE_KEY_UP }; + sticky_keys_filter.notify_key(&event); + assert_eq!(test_filter.last_event().unwrap(), event); + } + } + + #[test] + fn test_notify_key_passes_non_modifier_keys() { + let test_filter = TestFilter::new(); + let test_callbacks = TestCallbacks::new(); + let mut sticky_keys_filter = setup_filter( + Box::new(test_filter.clone()), + Arc::new(RwLock::new(Strong::new(Box::new(test_callbacks.clone())))), + ); + let event = KeyEvent { keyCode: KEY_A, ..BASE_KEY_DOWN }; + sticky_keys_filter.notify_key(&event); + assert_eq!(test_filter.last_event().unwrap(), event); + + let event = KeyEvent { keyCode: KEY_A, ..BASE_KEY_UP }; + sticky_keys_filter.notify_key(&event); + assert_eq!(test_filter.last_event().unwrap(), event); + } + + #[test] + fn test_modifier_state_updated_on_modifier_key_press() { + let mut test_filter = TestFilter::new(); + let mut test_callbacks = TestCallbacks::new(); + let mut sticky_keys_filter = setup_filter( + Box::new(test_filter.clone()), + Arc::new(RwLock::new(Strong::new(Box::new(test_callbacks.clone())))), + ); + let test_states = &[ + (KEYCODE_ALT_LEFT, META_ALT_ON | META_ALT_LEFT_ON), + (KEYCODE_ALT_RIGHT, META_ALT_ON | META_ALT_RIGHT_ON), + (KEYCODE_CTRL_LEFT, META_CTRL_ON | META_CTRL_LEFT_ON), + (KEYCODE_CTRL_RIGHT, META_CTRL_ON | META_CTRL_RIGHT_ON), + (KEYCODE_SHIFT_LEFT, META_SHIFT_ON | META_SHIFT_LEFT_ON), + (KEYCODE_SHIFT_RIGHT, META_SHIFT_ON | META_SHIFT_RIGHT_ON), + (KEYCODE_META_LEFT, META_META_ON | META_META_LEFT_ON), + (KEYCODE_META_RIGHT, META_META_ON | META_META_RIGHT_ON), + ]; + for test_state in test_states.iter() { + test_filter.clear(); + test_callbacks.clear(); + sticky_keys_filter.notify_key(&KeyEvent { keyCode: test_state.0, ..BASE_KEY_DOWN }); + assert_eq!(test_callbacks.get_last_modifier_state(), 0); + assert_eq!(test_callbacks.get_last_locked_modifier_state(), 0); + + sticky_keys_filter.notify_key(&KeyEvent { keyCode: test_state.0, ..BASE_KEY_UP }); + assert_eq!(test_callbacks.get_last_modifier_state(), test_state.1); + assert_eq!(test_callbacks.get_last_locked_modifier_state(), 0); + + // Re-send keys to lock it + sticky_keys_filter.notify_key(&KeyEvent { keyCode: test_state.0, ..BASE_KEY_DOWN }); + assert_eq!(test_callbacks.get_last_modifier_state(), test_state.1); + assert_eq!(test_callbacks.get_last_locked_modifier_state(), 0); + + sticky_keys_filter.notify_key(&KeyEvent { keyCode: test_state.0, ..BASE_KEY_UP }); + assert_eq!(test_callbacks.get_last_modifier_state(), test_state.1); + assert_eq!(test_callbacks.get_last_locked_modifier_state(), test_state.1); + + // Re-send keys to clear + sticky_keys_filter.notify_key(&KeyEvent { keyCode: test_state.0, ..BASE_KEY_DOWN }); + assert_eq!(test_callbacks.get_last_modifier_state(), test_state.1); + assert_eq!(test_callbacks.get_last_locked_modifier_state(), test_state.1); + + sticky_keys_filter.notify_key(&KeyEvent { keyCode: test_state.0, ..BASE_KEY_UP }); + assert_eq!(test_callbacks.get_last_modifier_state(), 0); + assert_eq!(test_callbacks.get_last_locked_modifier_state(), 0); + } + } + + #[test] + fn test_modifier_state_cleared_on_non_modifier_key_press() { + let test_filter = TestFilter::new(); + let test_callbacks = TestCallbacks::new(); + let mut sticky_keys_filter = setup_filter( + Box::new(test_filter.clone()), + Arc::new(RwLock::new(Strong::new(Box::new(test_callbacks.clone())))), + ); + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEYCODE_CTRL_LEFT, ..BASE_KEY_DOWN }); + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEYCODE_CTRL_LEFT, ..BASE_KEY_UP }); + + assert_eq!(test_callbacks.get_last_modifier_state(), META_CTRL_LEFT_ON | META_CTRL_ON); + assert_eq!(test_callbacks.get_last_locked_modifier_state(), 0); + + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEY_A, ..BASE_KEY_DOWN }); + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEY_A, ..BASE_KEY_UP }); + + assert_eq!(test_callbacks.get_last_modifier_state(), 0); + assert_eq!(test_callbacks.get_last_locked_modifier_state(), 0); + } + + #[test] + fn test_locked_modifier_state_not_cleared_on_non_modifier_key_press() { + let test_filter = TestFilter::new(); + let test_callbacks = TestCallbacks::new(); + let mut sticky_keys_filter = setup_filter( + Box::new(test_filter.clone()), + Arc::new(RwLock::new(Strong::new(Box::new(test_callbacks.clone())))), + ); + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEYCODE_CTRL_LEFT, ..BASE_KEY_DOWN }); + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEYCODE_CTRL_LEFT, ..BASE_KEY_UP }); + + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEYCODE_CTRL_LEFT, ..BASE_KEY_DOWN }); + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEYCODE_CTRL_LEFT, ..BASE_KEY_UP }); + + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEYCODE_SHIFT_LEFT, ..BASE_KEY_DOWN }); + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEYCODE_SHIFT_LEFT, ..BASE_KEY_UP }); + + assert_eq!( + test_callbacks.get_last_modifier_state(), + META_SHIFT_LEFT_ON | META_SHIFT_ON | META_CTRL_LEFT_ON | META_CTRL_ON + ); + assert_eq!( + test_callbacks.get_last_locked_modifier_state(), + META_CTRL_LEFT_ON | META_CTRL_ON + ); + + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEY_A, ..BASE_KEY_DOWN }); + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEY_A, ..BASE_KEY_UP }); + + assert_eq!(test_callbacks.get_last_modifier_state(), META_CTRL_LEFT_ON | META_CTRL_ON); + assert_eq!( + test_callbacks.get_last_locked_modifier_state(), + META_CTRL_LEFT_ON | META_CTRL_ON + ); + } + + #[test] + fn test_key_events_have_sticky_modifier_state() { + let test_filter = TestFilter::new(); + let test_callbacks = TestCallbacks::new(); + let mut sticky_keys_filter = setup_filter( + Box::new(test_filter.clone()), + Arc::new(RwLock::new(Strong::new(Box::new(test_callbacks.clone())))), + ); + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEYCODE_CTRL_LEFT, ..BASE_KEY_DOWN }); + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEYCODE_CTRL_LEFT, ..BASE_KEY_UP }); + + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEY_A, ..BASE_KEY_DOWN }); + assert_eq!( + test_filter.last_event().unwrap().metaState as u32, + META_CTRL_LEFT_ON | META_CTRL_ON + ); + + sticky_keys_filter.notify_key(&KeyEvent { keyCode: KEY_A, ..BASE_KEY_UP }); + assert_eq!( + test_filter.last_event().unwrap().metaState as u32, + META_CTRL_LEFT_ON | META_CTRL_ON + ); + } + + #[test] + fn test_modifier_state_not_cleared_until_all_devices_removed() { + let test_filter = TestFilter::new(); + let test_callbacks = TestCallbacks::new(); + let mut sticky_keys_filter = setup_filter( + Box::new(test_filter.clone()), + Arc::new(RwLock::new(Strong::new(Box::new(test_callbacks.clone())))), + ); + sticky_keys_filter.notify_key(&KeyEvent { + deviceId: 1, + keyCode: KEYCODE_CTRL_LEFT, + ..BASE_KEY_DOWN + }); + sticky_keys_filter.notify_key(&KeyEvent { + deviceId: 1, + keyCode: KEYCODE_CTRL_LEFT, + ..BASE_KEY_UP + }); + + sticky_keys_filter.notify_key(&KeyEvent { + deviceId: 2, + keyCode: KEYCODE_CTRL_LEFT, + ..BASE_KEY_DOWN + }); + sticky_keys_filter.notify_key(&KeyEvent { + deviceId: 2, + keyCode: KEYCODE_CTRL_LEFT, + ..BASE_KEY_UP + }); + + sticky_keys_filter.notify_devices_changed(&[DeviceInfo { deviceId: 2, external: true }]); + assert_eq!(test_callbacks.get_last_modifier_state(), META_CTRL_LEFT_ON | META_CTRL_ON); + assert_eq!( + test_callbacks.get_last_locked_modifier_state(), + META_CTRL_LEFT_ON | META_CTRL_ON + ); + + sticky_keys_filter.notify_devices_changed(&[]); + assert_eq!(test_callbacks.get_last_modifier_state(), 0); + assert_eq!(test_callbacks.get_last_locked_modifier_state(), 0); + } + + fn setup_filter( + next: Box<dyn Filter + Send + Sync>, + callbacks: Arc<RwLock<Strong<dyn IInputFilterCallbacks>>>, + ) -> StickyKeysFilter { + StickyKeysFilter::new(next, ModifierStateListener::new(callbacks)) + } +} |