blob: 7277f25b90b031e0311c1cf6524fef43b7454e90 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! # Fixups for the Chromebook keyboard
//!
//! Chromebook keyboards have a top row of "action" keys, which are usually
//! reported as function keys. The correct functionality would be to allow
//! them to be used as either function or action keys, depending on whether
//! the "search" key is being actuated alongside one of the keys.
//!
//! The "Search" key in Chromebooks is used to substitute for a range of keys
//! that are not usually present on a Chromebook keyboard. This Handler
//! implements some of those.
use crate::input_device::{
Handled, InputDeviceDescriptor, InputDeviceEvent, InputEvent, UnhandledInputEvent,
};
use crate::input_handler::{InputHandlerStatus, UnhandledInputHandler};
use crate::keyboard_binding::{KeyboardDeviceDescriptor, KeyboardEvent};
use async_trait::async_trait;
use fidl_fuchsia_input::Key;
use fidl_fuchsia_ui_input3::KeyEventType;
use fuchsia_inspect::health::Reporter;
use fuchsia_trace as ftrace;
use fuchsia_zircon as zx;
use keymaps::KeyState;
use lazy_static::lazy_static;
use maplit::hashmap;
use std::{cell::RefCell, rc::Rc};
/// The vendor ID denoting the internal Chromebook keyboard.
const VENDOR_ID: u32 = 0x18d1; // Google
/// The product ID denoting the internal Chromebook keyboard.
const PRODUCT_ID: u32 = 0x10003;
//// The Chromebook "Search" key is reported as left meta.
const SEARCH_KEY: Key = Key::LeftMeta;
#[derive(Debug)]
struct KeyPair {
/// The emitted key without actuated Search key.
without_search: Key,
/// The emitted key with actuated Search key.
with_search: Key,
}
lazy_static! {
// Map key is the original key code produced by the keyboard. The map value
// are the possible remapped keys, depending on whether Search key is
// actuated.
static ref REMAPPED_KEYS: std::collections::HashMap<Key, KeyPair> = hashmap! {
Key::F1 => KeyPair{ without_search: Key::AcBack, with_search: Key::F1 },
Key::F2 => KeyPair{ without_search: Key::AcRefresh, with_search: Key::F2},
Key::F3 => KeyPair{ without_search: Key::AcFullScreenView, with_search: Key::F3 },
Key::F4 => KeyPair{ without_search: Key::AcSelectTaskApplication, with_search: Key::F4 },
Key::F5 => KeyPair{ without_search: Key::BrightnessDown, with_search: Key::F5 },
Key::F6 => KeyPair{ without_search: Key::BrightnessUp, with_search: Key::F6 },
Key::F7 => KeyPair{ without_search: Key::PlayPause, with_search: Key::F7 },
Key::F8 => KeyPair{ without_search: Key::Mute, with_search: Key::F8 },
Key::F9 => KeyPair{ without_search: Key::VolumeDown, with_search: Key::F9 },
Key::F10 => KeyPair{ without_search: Key::VolumeUp, with_search: Key::F10 },
Key::Left => KeyPair{ without_search: Key::Left, with_search: Key::Home },
Key::Right => KeyPair{ without_search: Key::Right, with_search: Key::End },
Key::Up => KeyPair{ without_search: Key::Up, with_search: Key::PageUp },
Key::Down => KeyPair{ without_search: Key::Down, with_search: Key::PageDown },
Key::Dot => KeyPair{ without_search: Key::Dot, with_search: Key::Insert },
Key::Backspace => KeyPair{ without_search: Key::Backspace, with_search: Key::Delete },
};
}
/// A Chromebook dedicated keyboard handler.
///
/// Create a new instance with [ChromebookKeyboardHandler::new].
#[derive(Debug, Default)]
pub struct ChromebookKeyboardHandler {
// Handler's mutable state must be accessed via RefCell.
state: RefCell<Inner>,
/// The inventory of this handler's Inspect status.
pub inspect_status: InputHandlerStatus,
}
#[derive(Debug, Default)]
struct Inner {
/// The list of keys (using original key codes, not remapped ones) that are
/// currently actuated.
key_state: KeyState,
/// Is the search key currently activated.
is_search_key_actuated: bool,
/// Were there any new keyboard events generated by this handler, or
/// received by this handler, since the Search key was last pressed?
other_key_events: bool,
/// The minimum timestamp that is still larger than the last observed
/// timestamp (i.e. last + 1ns). Used to generate monotonically increasing
/// timestamps for generated events.
next_event_time: zx::Time,
/// Have any regular (non-remapped) keys been pressed since the actuation
/// of the Search key?
regular_keys_pressed: bool,
}
/// Returns true if the provided device info matches the Chromebook keyboard.
fn is_chromebook_keyboard(device_info: &fidl_fuchsia_input_report::DeviceInformation) -> bool {
device_info.product_id.unwrap_or_default() == PRODUCT_ID
&& device_info.vendor_id.unwrap_or_default() == VENDOR_ID
}
#[async_trait(?Send)]
impl UnhandledInputHandler for ChromebookKeyboardHandler {
async fn handle_unhandled_input_event(
self: Rc<Self>,
input_event: UnhandledInputEvent,
) -> Vec<InputEvent> {
match input_event.clone() {
// Decorate a keyboard event with key meaning.
UnhandledInputEvent {
device_event: InputDeviceEvent::Keyboard(event),
device_descriptor: InputDeviceDescriptor::Keyboard(ref keyboard_descriptor),
event_time,
trace_id,
} if is_chromebook_keyboard(&keyboard_descriptor.device_information) => {
self.inspect_status.count_received_event(InputEvent::from(input_event));
self.process_keyboard_event(
event,
keyboard_descriptor.clone(),
event_time,
trace_id,
)
}
// Pass other events unchanged.
_ => vec![InputEvent::from(input_event)],
}
}
fn set_handler_healthy(self: std::rc::Rc<Self>) {
self.inspect_status.health_node.borrow_mut().set_ok();
}
fn set_handler_unhealthy(self: std::rc::Rc<Self>, msg: &str) {
self.inspect_status.health_node.borrow_mut().set_unhealthy(msg);
}
}
impl ChromebookKeyboardHandler {
/// Creates a new instance of the handler.
pub fn new(input_handlers_node: &fuchsia_inspect::Node) -> Rc<Self> {
let inspect_status = InputHandlerStatus::new(
input_handlers_node,
"chromebook_keyboard_handler",
/* generates_events */ true,
);
Rc::new(Self { state: RefCell::new(Default::default()), inspect_status })
}
/// Gets the next event time that is at least as large as event_time, and
/// larger than last seen time.
fn next_event_time(self: &Rc<Self>, event_time: zx::Time) -> zx::Time {
let proposed = self.state.borrow().next_event_time;
let returned = if event_time < proposed { proposed } else { event_time };
self.state.borrow_mut().next_event_time = returned + zx::Duration::from_nanos(1);
returned
}
/// Updates the internal key state, but only for remappable keys.
fn update_key_state(self: &Rc<Self>, event_type: KeyEventType, key: Key) {
if REMAPPED_KEYS.contains_key(&key) {
self.state.borrow_mut().key_state.update(event_type, key)
}
}
/// Gets the list of keys in the key state, in order they were pressed.
fn get_ordered_keys(self: &Rc<Self>) -> Vec<Key> {
self.state.borrow().key_state.get_ordered_keys()
}
fn is_search_key_actuated(self: &Rc<Self>) -> bool {
self.state.borrow().is_search_key_actuated
}
fn set_search_key_actuated(self: &Rc<Self>, value: bool) {
self.state.borrow_mut().is_search_key_actuated = value;
}
fn has_other_key_events(self: &Rc<Self>) -> bool {
self.state.borrow().other_key_events
}
fn set_other_key_events(self: &Rc<Self>, value: bool) {
self.state.borrow_mut().other_key_events = value;
}
fn is_regular_keys_pressed(self: &Rc<Self>) -> bool {
self.state.borrow().regular_keys_pressed
}
fn set_regular_keys_pressed(self: &Rc<Self>, value: bool) {
self.state.borrow_mut().regular_keys_pressed = value;
}
fn synthesize_input_events<'a, I: Iterator<Item = &'a Key>>(
self: &Rc<Self>,
event: KeyboardEvent,
event_type: KeyEventType,
descriptor: KeyboardDeviceDescriptor,
event_time: zx::Time,
trace_id: Option<ftrace::Id>,
keys: I,
mapfn: fn(&KeyPair) -> Key,
) -> Vec<InputEvent> {
keys.map(|key| {
mapfn(REMAPPED_KEYS.get(key).expect("released_key must be in REMAPPED_KEYS"))
})
.map(|key| {
into_unhandled_input_event(
event.clone().into_with_key(key).into_with_event_type(event_type),
descriptor.clone(),
self.next_event_time(event_time),
trace_id,
)
})
.collect()
}
/// Remaps hardware events.
fn process_keyboard_event(
self: &Rc<Self>,
event: KeyboardEvent,
device_descriptor: KeyboardDeviceDescriptor,
event_time: zx::Time,
trace_id: Option<ftrace::Id>,
) -> Vec<InputEvent> {
// Remapping happens when search key is *not* actuated. The keyboard
// sends the F1 key code, but we must convert it by default to AcBack,
// for example.
let is_search_key_actuated = self.is_search_key_actuated();
let key = event.get_key();
let event_type_folded = event.get_event_type_folded();
let event_type = event.get_event_type();
match is_search_key_actuated {
true => {
// If the meta key is released, turn the remapping off, but also flip remapping of
// any currently active keys.
if key == SEARCH_KEY && event_type_folded == KeyEventType::Released {
// Used to synthesize new events.
let keys_to_release = self.get_ordered_keys();
// No more remapping: flip any active keys to non-remapped, and continue.
let mut new_events = self.synthesize_input_events(
event.clone(),
KeyEventType::Released,
device_descriptor.clone(),
event_time,
None,
keys_to_release.iter().rev(),
|kp: &KeyPair| kp.with_search,
);
new_events.append(&mut self.synthesize_input_events(
event.clone(),
KeyEventType::Pressed,
device_descriptor.clone(),
event_time,
None,
keys_to_release.iter(),
|kp: &KeyPair| kp.without_search,
));
// The Search key serves a dual purpose: it is a "silent" modifier for
// action keys, and it also can serve as a left Meta key if pressed on
// its own, or in combination with a key that does not normally get
// remapped. Such would be the case of Meta+A, where we must synthesize
// a Meta press, then press of A, then the respective releases. Contrast
// to Search+AcBack, which only results in F1, without Meta.
let search_key_only =
!self.has_other_key_events() && event_type == KeyEventType::Released;
// If there were no intervening events between a press and a release of the
// Search key, then emulate a press.
if search_key_only {
new_events.push(into_unhandled_input_event(
event.clone().into_with_event_type(KeyEventType::Pressed),
device_descriptor.clone(),
self.next_event_time(event_time),
None,
));
}
// Similarly, emulate a release too, in two cases:
//
// 1) No intervening presses (like above); and
// 2) There was a non-remapped key used with Search.
if search_key_only || self.is_regular_keys_pressed() {
new_events.push(into_unhandled_input_event(
event.clone().into_with_event_type(KeyEventType::Released),
device_descriptor.clone(),
self.next_event_time(event_time),
None,
));
}
// Reset search key state tracking to initial values.
self.set_search_key_actuated(false);
self.set_other_key_events(false);
self.set_regular_keys_pressed(false);
return new_events;
} else {
// Any other key press or release that is not the Search key.
}
}
false => {
if key == SEARCH_KEY && event_type == KeyEventType::Pressed {
// Used to synthesize new events.
let keys_to_release = self.get_ordered_keys();
let mut new_events = self.synthesize_input_events(
event.clone(),
KeyEventType::Released,
device_descriptor.clone(),
event_time,
None,
keys_to_release.iter().rev(),
|kp: &KeyPair| kp.without_search,
);
new_events.append(&mut self.synthesize_input_events(
event.clone(),
KeyEventType::Pressed,
device_descriptor.clone(),
event_time,
None,
keys_to_release.iter(),
|kp: &KeyPair| kp.with_search,
));
self.set_search_key_actuated(true);
if !keys_to_release.is_empty() {
self.set_other_key_events(true);
}
return new_events;
}
}
}
self.update_key_state(event_type, key);
let maybe_remapped_key = REMAPPED_KEYS.get(&key);
let return_events = if let Some(remapped_keypair) = maybe_remapped_key {
let key = if is_search_key_actuated {
remapped_keypair.with_search
} else {
remapped_keypair.without_search
};
vec![into_unhandled_input_event(
event.into_with_key(key),
device_descriptor,
self.next_event_time(event_time),
trace_id,
)]
} else {
let mut events = vec![];
// If this is the first non-remapped keypress after SEARCH_KEY actuation, we must emit
// the modifier before the key itself, because now we know that the user's intent was
// to use a modifier, not to remap action keys into function keys.
if self.is_search_key_actuated()
&& !self.has_other_key_events()
&& event_type == KeyEventType::Pressed
{
let new_event = event
.clone()
.into_with_key(SEARCH_KEY)
.into_with_event_type(KeyEventType::Pressed);
events.push(into_unhandled_input_event(
new_event,
device_descriptor.clone(),
self.next_event_time(event_time),
None,
));
self.set_regular_keys_pressed(true);
}
events.push(into_unhandled_input_event(
event,
device_descriptor,
self.next_event_time(event_time),
trace_id,
));
//
// Set "non-remapped-key".
events
};
// Remember that there were keypresses other than SEARCH_KEY after
// SEARCH_KEY was actuated.
if event_type == KeyEventType::Pressed && key != SEARCH_KEY && is_search_key_actuated {
self.set_other_key_events(true);
}
return_events
}
}
fn into_unhandled_input_event(
event: KeyboardEvent,
device_descriptor: KeyboardDeviceDescriptor,
event_time: zx::Time,
trace_id: Option<ftrace::Id>,
) -> InputEvent {
InputEvent {
device_event: InputDeviceEvent::Keyboard(event),
device_descriptor: device_descriptor.into(),
event_time,
handled: Handled::No,
trace_id,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing_utilities::create_input_event;
use test_case::test_case;
lazy_static! {
static ref MATCHING_KEYBOARD_DESCRIPTOR: InputDeviceDescriptor =
InputDeviceDescriptor::Keyboard(KeyboardDeviceDescriptor {
keys: vec![],
device_information: fidl_fuchsia_input_report::DeviceInformation {
vendor_id: Some(VENDOR_ID),
product_id: Some(PRODUCT_ID),
version: Some(42),
polling_rate: Some(1000),
..Default::default()
},
device_id: 43,
});
static ref MISMATCHING_KEYBOARD_DESCRIPTOR: InputDeviceDescriptor =
InputDeviceDescriptor::Keyboard(KeyboardDeviceDescriptor {
keys: vec![],
device_information: fidl_fuchsia_input_report::DeviceInformation {
vendor_id: Some(VENDOR_ID + 10),
product_id: Some(PRODUCT_ID),
version: Some(42),
polling_rate: Some(1000),
..Default::default()
},
device_id: 43,
});
}
async fn run_all_events<T: UnhandledInputHandler>(
handler: &Rc<T>,
events: Vec<InputEvent>,
) -> Vec<InputEvent> {
let handler_clone = || handler.clone();
let events_futs = events
.into_iter()
.map(|e| e.try_into().expect("events are always convertible in tests"))
.map(|e| handler_clone().handle_unhandled_input_event(e));
// Is there a good streaming way to achieve this?
let mut events_set = vec![];
for events_fut in events_futs.into_iter() {
events_set.push(events_fut.await);
}
events_set.into_iter().flatten().collect()
}
// A shorthand to create a sequence of keyboard events for testing. All events
// created share a descriptor and a handled marker. The event time is incremented
// for each event in turn.
fn new_key_sequence(
mut event_time: zx::Time,
descriptor: &InputDeviceDescriptor,
handled: Handled,
keys: Vec<(Key, KeyEventType)>,
) -> Vec<InputEvent> {
let mut ret = vec![];
for (k, t) in keys {
ret.push(create_input_event(KeyboardEvent::new(k, t), descriptor, event_time, handled));
event_time = event_time + zx::Duration::from_nanos(1);
}
ret
}
#[test]
fn next_event_time() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler = ChromebookKeyboardHandler::new(&test_node);
assert_eq!(zx::Time::from_nanos(10), handler.next_event_time(zx::Time::from_nanos(10)));
assert_eq!(zx::Time::from_nanos(11), handler.next_event_time(zx::Time::from_nanos(10)));
assert_eq!(zx::Time::from_nanos(12), handler.next_event_time(zx::Time::from_nanos(10)));
assert_eq!(zx::Time::from_nanos(13), handler.next_event_time(zx::Time::from_nanos(13)));
assert_eq!(zx::Time::from_nanos(14), handler.next_event_time(zx::Time::from_nanos(13)));
}
// Basic tests: ensure that function key codes are normally converted into
// action key codes on the built-in keyboard. Other action keys are not.
#[test_case(Key::F1, Key::AcBack; "convert F1")]
#[test_case(Key::F2, Key::AcRefresh; "convert F2")]
#[test_case(Key::F3, Key::AcFullScreenView; "convert F3")]
#[test_case(Key::F4, Key::AcSelectTaskApplication; "convert F4")]
#[test_case(Key::F5, Key::BrightnessDown; "convert F5")]
#[test_case(Key::F6, Key::BrightnessUp; "convert F6")]
#[test_case(Key::F7, Key::PlayPause; "convert F7")]
#[test_case(Key::F8, Key::Mute; "convert F8")]
#[test_case(Key::F9, Key::VolumeDown; "convert F9")]
#[test_case(Key::F10, Key::VolumeUp; "convert F10")]
#[test_case(Key::A, Key::A; "do not convert A")]
#[test_case(Key::Up, Key::Up; "do not convert Up")]
#[test_case(Key::Down, Key::Down; "do not convert Down")]
#[test_case(Key::Left, Key::Left; "do not convert Left")]
#[test_case(Key::Right, Key::Right; "do not convert Right")]
#[test_case(Key::Dot, Key::Dot; "do not convert Dot")]
#[test_case(Key::Backspace, Key::Backspace; "do not convert Backspace")]
#[fuchsia::test]
async fn conversion_matching_keyboard(input_key: Key, output_key: Key) {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler = ChromebookKeyboardHandler::new(&test_node);
let input = new_key_sequence(
zx::Time::from_nanos(42),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![(input_key, KeyEventType::Pressed), (input_key, KeyEventType::Released)],
);
let actual = run_all_events(&handler, input).await;
let expected = new_key_sequence(
zx::Time::from_nanos(42),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![(output_key, KeyEventType::Pressed), (output_key, KeyEventType::Released)],
);
pretty_assertions::assert_eq!(expected, actual);
}
// Basic tests: ensure that function key codes are NOT converted into
// action key codes on any other keyboard (e.g. external).
#[test_case(Key::F1, Key::F1; "do not convert F1")]
#[test_case(Key::F2, Key::F2; "do not convert F2")]
#[test_case(Key::F3, Key::F3; "do not convert F3")]
#[test_case(Key::F4, Key::F4; "do not convert F4")]
#[test_case(Key::F5, Key::F5; "do not convert F5")]
#[test_case(Key::F6, Key::F6; "do not convert F6")]
#[test_case(Key::F7, Key::F7; "do not convert F7")]
#[test_case(Key::F8, Key::F8; "do not convert F8")]
#[test_case(Key::F9, Key::F9; "do not convert F9")]
#[test_case(Key::F10, Key::F10; "do not convert F10")]
#[test_case(Key::A, Key::A; "do not convert A")]
#[fuchsia::test]
async fn conversion_mismatching_keyboard(input_key: Key, output_key: Key) {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler = ChromebookKeyboardHandler::new(&test_node);
let input = new_key_sequence(
zx::Time::from_nanos(42),
&MISMATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![(input_key, KeyEventType::Pressed), (input_key, KeyEventType::Released)],
);
let actual = run_all_events(&handler, input).await;
let expected = new_key_sequence(
zx::Time::from_nanos(42),
&MISMATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![(output_key, KeyEventType::Pressed), (output_key, KeyEventType::Released)],
);
pretty_assertions::assert_eq!(expected, actual);
}
// If a Search key is pressed without intervening keypresses, it results in
// a delayed press and release.
//
// SEARCH_KEY[in] ___/""""""""\___
//
// SEARCH_KEY[out] ____________/""\___
#[fuchsia::test]
async fn search_key_only() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler = ChromebookKeyboardHandler::new(&test_node);
let input = new_key_sequence(
zx::Time::from_nanos(42),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![(SEARCH_KEY, KeyEventType::Pressed), (SEARCH_KEY, KeyEventType::Released)],
);
let actual = run_all_events(&handler, input).await;
let expected = new_key_sequence(
zx::Time::from_nanos(43),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![(SEARCH_KEY, KeyEventType::Pressed), (SEARCH_KEY, KeyEventType::Released)],
);
pretty_assertions::assert_eq!(expected, actual);
}
// When a remappable key (e.g. F1) is pressed with the Search key, the effect
// is as if the action key only is pressed.
//
// SEARCH_KEY[in] ___/"""""""""""\___
// F1[in] ______/""""\_______
//
// SEARCH_KEY[out] ___________________
// F1[out] ______/""""\_______
#[fuchsia::test]
async fn f1_conversion() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler = ChromebookKeyboardHandler::new(&test_node);
let input = new_key_sequence(
zx::Time::from_nanos(42),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![
(SEARCH_KEY, KeyEventType::Pressed),
(Key::F1, KeyEventType::Pressed),
(Key::F1, KeyEventType::Released),
(SEARCH_KEY, KeyEventType::Released),
],
);
let actual = run_all_events(&handler, input).await;
let expected = new_key_sequence(
zx::Time::from_nanos(43),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![(Key::F1, KeyEventType::Pressed), (Key::F1, KeyEventType::Released)],
);
pretty_assertions::assert_eq!(expected, actual);
}
// When a remappable key (e.g. F1) is pressed with the Search key, the effect
// is as if the action key only is pressed.
//
// SEARCH_KEY[in] ___/"""""""""""\___
// F1[in] ______/""""\_______
//
// SEARCH_KEY[out] ___________________
// F1[out] ______/""""\_______
//
// Similar tests are ran on all other convertible keys.
#[test_case(Key::F1, Key::F1; "do not convert F1")]
#[test_case(Key::F2, Key::F2; "do not convert F2")]
#[test_case(Key::F3, Key::F3; "do not convert F3")]
#[test_case(Key::F4, Key::F4; "do not convert F4")]
#[test_case(Key::F5, Key::F5; "do not convert F5")]
#[test_case(Key::F6, Key::F6; "do not convert F6")]
#[test_case(Key::F7, Key::F7; "do not convert F7")]
#[test_case(Key::F8, Key::F8; "do not convert F8")]
#[test_case(Key::F9, Key::F9; "do not convert F9")]
#[test_case(Key::F10, Key::F10; "do not convert F10")]
#[test_case(Key::Up, Key::PageUp; "convert Up")]
#[test_case(Key::Down, Key::PageDown; "convert Down")]
#[test_case(Key::Left, Key::Home; "convert Left")]
#[test_case(Key::Right, Key::End; "convert Right")]
#[test_case(Key::Dot, Key::Insert; "convert Dot")]
#[test_case(Key::Backspace, Key::Delete; "convert Backspace")]
#[fuchsia::test]
async fn with_search_key_pressed(input_key: Key, output_key: Key) {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler = ChromebookKeyboardHandler::new(&test_node);
let input = new_key_sequence(
zx::Time::from_nanos(42),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![
(SEARCH_KEY, KeyEventType::Pressed),
(input_key, KeyEventType::Pressed),
(input_key, KeyEventType::Released),
(SEARCH_KEY, KeyEventType::Released),
],
);
let actual = run_all_events(&handler, input).await;
let expected = new_key_sequence(
zx::Time::from_nanos(43),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![(output_key, KeyEventType::Pressed), (output_key, KeyEventType::Released)],
);
pretty_assertions::assert_eq!(expected, actual);
}
// SEARCH_KEY[in] __/""""""\________
// F1[in] _____/""""""""\___
//
// SEARCH_KEY[out] __________________
// F1[out] _____/"""\________
// AcBack[out] _________/""""\___
#[fuchsia::test]
async fn search_released_before_f1() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler = ChromebookKeyboardHandler::new(&test_node);
let input = new_key_sequence(
zx::Time::from_nanos(42),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![
(SEARCH_KEY, KeyEventType::Pressed),
(Key::F1, KeyEventType::Pressed),
(SEARCH_KEY, KeyEventType::Released),
(Key::F1, KeyEventType::Released),
],
);
let actual = run_all_events(&handler, input).await;
let expected = new_key_sequence(
zx::Time::from_nanos(43),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![
(Key::F1, KeyEventType::Pressed),
(Key::F1, KeyEventType::Released),
(Key::AcBack, KeyEventType::Pressed),
(Key::AcBack, KeyEventType::Released),
],
);
pretty_assertions::assert_eq!(expected, actual);
}
// When a "regular" key (e.g. "A") is pressed in chord with the Search key,
// the effect is as if LeftMeta+A was pressed.
//
// SEARCH_KEY[in] ___/"""""""""""\__
// A[in] _____/""""\_______
//
// SEARCH_KEY[out] _____/"""""""""\__
// A[out] ______/""""\______
#[fuchsia::test]
async fn search_key_a_is_delayed_leftmeta_a() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler = ChromebookKeyboardHandler::new(&test_node);
let input = new_key_sequence(
zx::Time::from_nanos(42),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![
(SEARCH_KEY, KeyEventType::Pressed),
(Key::A, KeyEventType::Pressed),
(Key::A, KeyEventType::Released),
(SEARCH_KEY, KeyEventType::Released),
],
);
let actual = run_all_events(&handler, input).await;
let expected = new_key_sequence(
zx::Time::from_nanos(43),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![
(Key::LeftMeta, KeyEventType::Pressed),
(Key::A, KeyEventType::Pressed),
(Key::A, KeyEventType::Released),
(Key::LeftMeta, KeyEventType::Released),
],
);
pretty_assertions::assert_eq!(expected, actual);
}
// SEARCH_KEY[in] ___/"""""""""""\___
// F1[in] ______/""""\_______
// F2[in] _________/""""\____
//
// SEARCH_KEY[out] ___________________
// F1[out] ______/""""\_______
// F2[out] _________/""""\____
#[fuchsia::test]
async fn f1_and_f2_interleaved_conversion() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler = ChromebookKeyboardHandler::new(&test_node);
let input = new_key_sequence(
zx::Time::from_nanos(42),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![
(SEARCH_KEY, KeyEventType::Pressed),
(Key::F1, KeyEventType::Pressed),
(Key::F2, KeyEventType::Pressed),
(Key::F1, KeyEventType::Released),
(Key::F2, KeyEventType::Released),
(SEARCH_KEY, KeyEventType::Released),
],
);
let actual = run_all_events(&handler, input).await;
let expected = new_key_sequence(
zx::Time::from_nanos(43),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![
(Key::F1, KeyEventType::Pressed),
(Key::F2, KeyEventType::Pressed),
(Key::F1, KeyEventType::Released),
(Key::F2, KeyEventType::Released),
],
);
pretty_assertions::assert_eq!(expected, actual);
}
// SEARCH_KEY[in] _______/""""""\__
// F1[in] ___/""""""""\____
//
// SEARCH_KEY[out] _________________
// F1[out] _______/"""""\___
// AcBack[out] __/""""\_________
#[fuchsia::test]
async fn search_pressed_before_f1_released() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler = ChromebookKeyboardHandler::new(&test_node);
let input = new_key_sequence(
zx::Time::from_nanos(42),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![
(Key::F1, KeyEventType::Pressed),
(SEARCH_KEY, KeyEventType::Pressed),
(Key::F1, KeyEventType::Released),
(SEARCH_KEY, KeyEventType::Released),
],
);
let actual = run_all_events(&handler, input).await;
let expected = new_key_sequence(
zx::Time::from_nanos(42),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![
(Key::AcBack, KeyEventType::Pressed),
(Key::AcBack, KeyEventType::Released),
(Key::F1, KeyEventType::Pressed),
(Key::F1, KeyEventType::Released),
],
);
pretty_assertions::assert_eq!(expected, actual);
}
// When the Search key gets actuated when there are already remappable keys
// actuated, we de-actuate the remapped versions, and actuate remapped ones.
// This causes the output to observe both F1, F2 and AcBack and AcRefresh.
//
// SEARCH_KEY[in] _______/""""""""""""\___
// F1[in] ___/"""""""""\__________
// F2[in] _____/""""""""""\_______
//
// SEARCH_KEY[out] ________________________
// F1[out] _______/"""""\__________
// AcBack[out] ___/"""\________________
// F2[out] _______/""""""""\_______
// AcRefresh[out] _____/"\________________
#[fuchsia::test]
async fn search_pressed_while_f1_and_f2_pressed() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler = ChromebookKeyboardHandler::new(&test_node);
let input = new_key_sequence(
zx::Time::from_nanos(42),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![
(Key::F1, KeyEventType::Pressed),
(Key::F2, KeyEventType::Pressed),
(SEARCH_KEY, KeyEventType::Pressed),
(Key::F1, KeyEventType::Released),
(Key::F2, KeyEventType::Released),
(SEARCH_KEY, KeyEventType::Released),
],
);
let actual = run_all_events(&handler, input).await;
let expected = new_key_sequence(
zx::Time::from_nanos(42),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![
(Key::AcBack, KeyEventType::Pressed),
(Key::AcRefresh, KeyEventType::Pressed),
(Key::AcRefresh, KeyEventType::Released),
(Key::AcBack, KeyEventType::Released),
(Key::F1, KeyEventType::Pressed),
(Key::F2, KeyEventType::Pressed),
(Key::F1, KeyEventType::Released),
(Key::F2, KeyEventType::Released),
],
);
pretty_assertions::assert_eq!(expected, actual);
}
// An interleaving of remapped (F1, F2) and non-remapped keys (A). In this
// case, A is presented without a modifier. This is not strictly correct,
// but is a simpler implementation for an unlikely key combination.
//
// SEARCH_KEY[in] _______/"""""""""\______
// F1[in] ___/""""""\_____________
// A[in] _________/"""""\________
// F2[in] ___________/""""""""\___
//
// SEARCH_KEY[out] _______________________
// F1[out] _______/""\_____________
// AcBack[out] __/"""\________________
// A[out] ________/"""""\________
// F2[out] __________/"""""\______
// AcRefresh[out] __________________/""\__
#[fuchsia::test]
async fn key_combination() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let handler = ChromebookKeyboardHandler::new(&test_node);
let input = new_key_sequence(
zx::Time::from_nanos(42),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![
(Key::F1, KeyEventType::Pressed),
(SEARCH_KEY, KeyEventType::Pressed),
(Key::A, KeyEventType::Pressed),
(Key::F1, KeyEventType::Released),
(Key::F2, KeyEventType::Pressed),
(Key::A, KeyEventType::Released),
(SEARCH_KEY, KeyEventType::Released),
(Key::F2, KeyEventType::Released),
],
);
let actual = run_all_events(&handler, input).await;
let expected = new_key_sequence(
zx::Time::from_nanos(42),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![
(Key::AcBack, KeyEventType::Pressed),
(Key::AcBack, KeyEventType::Released),
(Key::F1, KeyEventType::Pressed),
(Key::A, KeyEventType::Pressed),
(Key::F1, KeyEventType::Released),
(Key::F2, KeyEventType::Pressed),
(Key::A, KeyEventType::Released),
(Key::F2, KeyEventType::Released),
(Key::AcRefresh, KeyEventType::Pressed),
(Key::AcRefresh, KeyEventType::Released),
],
);
pretty_assertions::assert_eq!(expected, actual);
}
#[fuchsia::test]
fn chromebook_keyboard_handler_initialized_with_inspect_node() {
let inspector = fuchsia_inspect::Inspector::default();
let fake_handlers_node = inspector.root().create_child("input_handlers_node");
let _handler = ChromebookKeyboardHandler::new(&fake_handlers_node);
diagnostics_assertions::assert_data_tree!(inspector, root: {
input_handlers_node: {
chromebook_keyboard_handler: {
events_received_count: 0u64,
events_handled_count: 0u64,
last_received_timestamp_ns: 0u64,
"fuchsia.inspect.Health": {
status: "STARTING_UP",
// Timestamp value is unpredictable and not relevant in this context,
// so we only assert that the property is present.
start_timestamp_nanos: diagnostics_assertions::AnyProperty
},
}
}
});
}
#[fuchsia::test]
async fn chromebook_keyboard_handler_inspect_counts_events() {
let inspector = fuchsia_inspect::Inspector::default();
let fake_handlers_node = inspector.root().create_child("input_handlers_node");
let handler = ChromebookKeyboardHandler::new(&fake_handlers_node);
let events = new_key_sequence(
zx::Time::from_nanos(42),
&MATCHING_KEYBOARD_DESCRIPTOR,
Handled::No,
vec![
(Key::F1, KeyEventType::Pressed),
(Key::F1, KeyEventType::Released),
(Key::Down, KeyEventType::Pressed),
(Key::Down, KeyEventType::Released),
],
);
let _ = run_all_events(&handler, events).await;
diagnostics_assertions::assert_data_tree!(inspector, root: {
input_handlers_node: {
chromebook_keyboard_handler: {
events_received_count: 4u64,
events_handled_count: 0u64,
last_received_timestamp_ns: 45u64,
"fuchsia.inspect.Health": {
status: "STARTING_UP",
// Timestamp value is unpredictable and not relevant in this context,
// so we only assert that the property is present.
start_timestamp_nanos: diagnostics_assertions::AnyProperty
},
}
}
});
}
}