Add sticky keys input filter
DD: go/pk_accessibility
'Sticky keys' is an accessibility feature that assists users
who have physical disabilities or help 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.
Bug: 294546335
Test: TEST=libinputflinger_rs_test; m $TEST && $ANDROID_HOST_OUT/nativetest64/$TEST/$TEST
Change-Id: Id26ed4a949e929cb6af8b9ecf1cd95c48fe20486
diff --git a/services/inputflinger/Android.bp b/services/inputflinger/Android.bp
index 45c9b5c..69f42bc 100644
--- a/services/inputflinger/Android.bp
+++ b/services/inputflinger/Android.bp
@@ -77,6 +77,7 @@
"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 9c4a3eb..5d87d34 100644
--- a/services/inputflinger/InputFilter.cpp
+++ b/services/inputflinger/InputFilter.cpp
@@ -44,31 +44,9 @@
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::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::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 06f7d0e..9fa7a87 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 @@
*/
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 @@
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 0000000..8c8f5e8
--- /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 0000000..c0a80fb
--- /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 14b41cd..2921d30 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 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 3b2e88b..38b1612 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 @@
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 340ff8e..e94a71f 100644
--- a/services/inputflinger/rust/input_filter.rs
+++ b/services/inputflinger/rust/input_filter.rs
@@ -27,6 +27,7 @@
};
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 @@
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 @@
}
}
+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 @@
#[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 @@
#[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 @@
#[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 @@
}
}
}
+
+#[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 68cd480..fa16898 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 0000000..da581b8
--- /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))
+ }
+}