| // Copyright 2024 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. |
| |
| use crate::InputFile; |
| use crate::input_event_relay::{DeviceId, OpenedFiles}; |
| use fuchsia_inspect::{NumericProperty, Property}; |
| use starnix_core::device::kobject::DeviceMetadata; |
| use starnix_core::device::{DeviceMode, DeviceOps}; |
| use starnix_core::task::CurrentTask; |
| use starnix_core::vfs::{FileOps, FsString, NamespaceNode}; |
| #[cfg(test)] |
| use starnix_sync::Unlocked; |
| use starnix_sync::{FileOpsCore, LockEqualOrBefore, Locked, Mutex}; |
| use starnix_uapi::device_type::{DeviceType, INPUT_MAJOR}; |
| use starnix_uapi::errors::Errno; |
| use starnix_uapi::open_flags::OpenFlags; |
| use starnix_uapi::{BUS_VIRTUAL, input_id}; |
| use std::sync::Arc; |
| |
| // Add a fuchsia-specific vendor ID. 0xfc1a is currently not allocated |
| // to any vendor in the USB spec. |
| // |
| // May not be zero, see below. |
| const FUCHSIA_VENDOR_ID: u16 = 0xfc1a; |
| |
| // May not be zero, see below. |
| const FUCHSIA_TOUCH_PRODUCT_ID: u16 = 0x2; |
| |
| // May not be zero, see below. |
| const FUCHSIA_KEYBOARD_PRODUCT_ID: u16 = 0x1; |
| |
| // May not be zero, see below. |
| const FUCHSIA_MOUSE_PRODUCT_ID: u16 = 0x3; |
| |
| // Touch, keyboard, and mouse input IDs should be distinct. |
| // Per https://www.linuxjournal.com/article/6429, the bus type should be populated with a |
| // sensible value, but other fields may not be. |
| // |
| // While this may be the case for Linux itself, Android is not so relaxed. |
| // Devices with apparently-invalid vendor or product IDs don't get extra |
| // device configuration. So we must make a minimum effort to present |
| // sensibly-looking product and vendor IDs. Zero version only means that |
| // version-specific config files will not be applied. |
| // |
| // For background, see: |
| // |
| // * Allowable file locations: |
| // https://source.android.com/docs/core/interaction/input/input-device-configuration-files#location |
| // * Android configuration selection code: |
| // https://source.corp.google.com/h/googleplex-android/platform/superproject/main/+/main:frameworks/native/libs/input/InputDevice.cpp;l=60;drc=285211e60bff87fc5a9c9b4105a4b4ccb7edffaf |
| const TOUCH_INPUT_ID: input_id = input_id { |
| bustype: BUS_VIRTUAL as u16, |
| // Make sure that vendor ID and product ID at least seem plausible. See |
| // above for details. |
| vendor: FUCHSIA_VENDOR_ID, |
| product: FUCHSIA_TOUCH_PRODUCT_ID, |
| // Version is OK to be zero, but config files named `Product_yyyy_Vendor_zzzz_Version_ttt.*` |
| // will not work. |
| version: 0, |
| }; |
| const KEYBOARD_INPUT_ID: input_id = input_id { |
| bustype: BUS_VIRTUAL as u16, |
| // Make sure that vendor ID and product ID at least seem plausible. See |
| // above for details. |
| vendor: FUCHSIA_VENDOR_ID, |
| product: FUCHSIA_KEYBOARD_PRODUCT_ID, |
| version: 1, |
| }; |
| |
| const MOUSE_INPUT_ID: input_id = input_id { |
| bustype: BUS_VIRTUAL as u16, |
| // Make sure that vendor ID and product ID at least seem plausible. See |
| // above for details. |
| vendor: FUCHSIA_VENDOR_ID, |
| product: FUCHSIA_MOUSE_PRODUCT_ID, |
| version: 1, |
| }; |
| |
| #[derive(Clone)] |
| enum InputDeviceType { |
| // A touch device, containing (display width, display height). |
| Touch(i32, i32), |
| |
| // A keyboard device. |
| Keyboard, |
| |
| // A mouse device. |
| Mouse, |
| } |
| |
| /// An [`InputDeviceStatus`] is tied to an [`InputDeviceBinding`] and provides properties |
| /// detailing its Inspect status. |
| /// We expect all (non-timestamp) properties' counts to equal the sum of that property from all |
| /// files opened on that device. So for example, if a device had 3 separate input files opened, we |
| /// would expect it's `total_fidl_events_received_count` to equal the sum of |
| /// `fidl_events_received_count` from all 3 files and so forth. |
| pub struct InputDeviceStatus { |
| /// A node that contains the state below. |
| pub node: fuchsia_inspect::Node, |
| |
| /// Hold onto inspect nodes for files opened on this device, so that when these files are |
| /// closed, their inspect data is maintained. |
| pub file_nodes: Mutex<Vec<fuchsia_inspect::Node>>, |
| |
| /// The number of FIDL events received by this device from Fuchsia input system. |
| /// |
| /// We expect: |
| /// total_fidl_events_received_count = total_fidl_events_ignored_count + |
| /// total_fidl_events_unexpected_count + |
| /// total_fidl_events_converted_count |
| /// otherwise starnix ignored events unexpectedly. |
| /// |
| /// total_fidl_events_unexpected_count should be 0, if not it hints issues from upstream of |
| /// ui stack. |
| pub total_fidl_events_received_count: fuchsia_inspect::UintProperty, |
| |
| /// The number of FIDL events ignored by this device when attempting conversion to this |
| /// module’s representation of a TouchEvent. |
| pub total_fidl_events_ignored_count: fuchsia_inspect::UintProperty, |
| |
| /// The unexpected number of FIDL events reached to this module should be filtered out |
| /// earlier in the UI stack. |
| /// It maybe unexpected format or unexpected order. |
| pub total_fidl_events_unexpected_count: fuchsia_inspect::UintProperty, |
| |
| /// The number of FIDL events converted by this device to this module’s representation of |
| /// TouchEvent. |
| pub total_fidl_events_converted_count: fuchsia_inspect::UintProperty, |
| |
| /// The number of uapi::input_events generated by this device from TouchEvents. |
| pub total_uapi_events_generated_count: fuchsia_inspect::UintProperty, |
| |
| /// The event time of the last generated uapi::input_event by one of this device's InputFiles. |
| pub last_generated_uapi_event_timestamp_ns: fuchsia_inspect::IntProperty, |
| } |
| |
| impl InputDeviceStatus { |
| fn new(node: fuchsia_inspect::Node) -> Self { |
| let total_fidl_events_received_count = |
| node.create_uint("total_fidl_events_received_count", 0); |
| let total_fidl_events_ignored_count = |
| node.create_uint("total_fidl_events_ignored_count", 0); |
| let total_fidl_events_unexpected_count = |
| node.create_uint("total_fidl_events_unexpected_count", 0); |
| let total_fidl_events_converted_count = |
| node.create_uint("total_fidl_events_converted_count", 0); |
| let total_uapi_events_generated_count = |
| node.create_uint("total_uapi_events_generated_count", 0); |
| let last_generated_uapi_event_timestamp_ns = |
| node.create_int("last_generated_uapi_event_timestamp_ns", 0); |
| Self { |
| node, |
| file_nodes: Mutex::new(vec![]), |
| total_fidl_events_received_count, |
| total_fidl_events_ignored_count, |
| total_fidl_events_unexpected_count, |
| total_fidl_events_converted_count, |
| total_uapi_events_generated_count, |
| last_generated_uapi_event_timestamp_ns, |
| } |
| } |
| |
| pub fn count_total_received_events(&self, count: u64) { |
| self.total_fidl_events_received_count.add(count); |
| } |
| |
| pub fn count_total_ignored_events(&self, count: u64) { |
| self.total_fidl_events_ignored_count.add(count); |
| } |
| |
| pub fn count_total_unexpected_events(&self, count: u64) { |
| self.total_fidl_events_unexpected_count.add(count); |
| } |
| |
| pub fn count_total_converted_events(&self, count: u64) { |
| self.total_fidl_events_converted_count.add(count); |
| } |
| |
| pub fn count_total_generated_events(&self, count: u64, event_time_ns: i64) { |
| self.total_uapi_events_generated_count.add(count); |
| self.last_generated_uapi_event_timestamp_ns.set(event_time_ns); |
| } |
| } |
| |
| #[derive(Clone)] |
| pub struct InputDevice { |
| device_type: InputDeviceType, |
| |
| pub open_files: OpenedFiles, |
| |
| pub inspect_status: Arc<InputDeviceStatus>, |
| } |
| |
| impl InputDevice { |
| pub fn new_touch( |
| display_width: i32, |
| display_height: i32, |
| inspect_node: &fuchsia_inspect::Node, |
| ) -> Self { |
| let node = inspect_node.create_child("touch_device"); |
| InputDevice { |
| device_type: InputDeviceType::Touch(display_width, display_height), |
| open_files: Default::default(), |
| inspect_status: Arc::new(InputDeviceStatus::new(node)), |
| } |
| } |
| |
| pub fn new_keyboard(inspect_node: &fuchsia_inspect::Node) -> Self { |
| let node = inspect_node.create_child("keyboard_device"); |
| InputDevice { |
| device_type: InputDeviceType::Keyboard, |
| open_files: Default::default(), |
| inspect_status: Arc::new(InputDeviceStatus::new(node)), |
| } |
| } |
| |
| pub fn new_mouse(inspect_node: &fuchsia_inspect::Node) -> Self { |
| let node = inspect_node.create_child("mouse_device"); |
| InputDevice { |
| device_type: InputDeviceType::Mouse, |
| open_files: Default::default(), |
| inspect_status: Arc::new(InputDeviceStatus::new(node)), |
| } |
| } |
| |
| pub fn register<L>( |
| self, |
| locked: &mut Locked<L>, |
| system_task: &CurrentTask, |
| device_id: DeviceId, |
| ) -> Result<(), Errno> |
| where |
| L: LockEqualOrBefore<FileOpsCore>, |
| { |
| let kernel = system_task.kernel(); |
| let registry = &kernel.device_registry; |
| |
| let input_class = registry.objects.input_class(); |
| registry.register_device( |
| locked, |
| system_task, |
| FsString::from(format!("event{}", device_id)).as_ref(), |
| DeviceMetadata::new( |
| format!("input/event{}", device_id).into(), |
| DeviceType::new(INPUT_MAJOR, device_id), |
| DeviceMode::Char, |
| ), |
| input_class, |
| self, |
| )?; |
| Ok(()) |
| } |
| |
| pub fn open_internal(&self) -> Box<dyn FileOps> { |
| let input_file = match self.device_type { |
| InputDeviceType::Touch(display_width, display_height) => { |
| let mut file_nodes = self.inspect_status.file_nodes.lock(); |
| let child_node = self |
| .inspect_status |
| .node |
| .create_child(format!("touch_file_{}", file_nodes.len())); |
| let file = Arc::new(InputFile::new_touch( |
| TOUCH_INPUT_ID, |
| display_width, |
| display_height, |
| Some(&child_node), |
| )); |
| file_nodes.push(child_node); |
| file |
| } |
| InputDeviceType::Keyboard => { |
| let mut file_nodes = self.inspect_status.file_nodes.lock(); |
| let child_node = self |
| .inspect_status |
| .node |
| .create_child(format!("keyboard_file_{}", file_nodes.len())); |
| let file = Arc::new(InputFile::new_keyboard(KEYBOARD_INPUT_ID, Some(&child_node))); |
| file_nodes.push(child_node); |
| file |
| } |
| InputDeviceType::Mouse => { |
| let mut file_nodes = self.inspect_status.file_nodes.lock(); |
| let child_node = self |
| .inspect_status |
| .node |
| .create_child(format!("mouse_file_{}", file_nodes.len())); |
| let file = Arc::new(InputFile::new_mouse(MOUSE_INPUT_ID, Some(&child_node))); |
| file_nodes.push(child_node); |
| file |
| } |
| }; |
| self.open_files.lock().push(Arc::downgrade(&input_file)); |
| Box::new(input_file) |
| } |
| |
| #[allow(clippy::unwrap_in_result, reason = "Force clippy rule in Starnix")] |
| #[cfg(test)] |
| pub fn open_test( |
| &self, |
| locked: &mut Locked<Unlocked>, |
| current_task: &CurrentTask, |
| ) -> Result<starnix_core::vfs::FileHandle, Errno> { |
| let input_file = self.open_internal(); |
| let file_object = starnix_core::vfs::FileObject::new( |
| current_task, |
| input_file, |
| current_task |
| .lookup_path_from_root(locked, ".".into()) |
| .expect("failed to get namespace node for root"), |
| OpenFlags::empty(), |
| ) |
| .expect("FileObject::new failed"); |
| Ok(file_object) |
| } |
| } |
| |
| impl DeviceOps for InputDevice { |
| fn open( |
| &self, |
| _locked: &mut Locked<FileOpsCore>, |
| _current_task: &CurrentTask, |
| _id: DeviceType, |
| _node: &NamespaceNode, |
| _flags: OpenFlags, |
| ) -> Result<Box<dyn FileOps>, Errno> { |
| let input_file = self.open_internal(); |
| Ok(input_file) |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| #![allow(clippy::unused_unit)] // for compatibility with `test_case` |
| |
| use super::*; |
| use crate::input_event_relay::{self, EventProxyMode}; |
| use anyhow::anyhow; |
| use assert_matches::assert_matches; |
| use diagnostics_assertions::{AnyProperty, assert_data_tree}; |
| use fidl_fuchsia_ui_input::MediaButtonsEvent; |
| use fuipointer::{ |
| EventPhase, TouchEvent, TouchInteractionId, TouchPointerSample, TouchResponse, |
| TouchSourceMarker, TouchSourceRequest, |
| }; |
| use futures::StreamExt as _; |
| use pretty_assertions::assert_eq; |
| use starnix_core::task::{EventHandler, Waiter}; |
| use starnix_core::testing::create_kernel_task_and_unlocked; |
| use starnix_core::vfs::FileHandle; |
| use starnix_core::vfs::buffers::VecOutputBuffer; |
| use starnix_types::time::timeval_from_time; |
| use starnix_uapi::errors::EAGAIN; |
| use starnix_uapi::uapi; |
| use starnix_uapi::vfs::FdEvents; |
| use test_case::test_case; |
| use test_util::assert_near; |
| use zerocopy::FromBytes as _; |
| use { |
| fidl_fuchsia_ui_input3 as fuiinput, fidl_fuchsia_ui_pointer as fuipointer, |
| fidl_fuchsia_ui_policy as fuipolicy, |
| }; |
| |
| const INPUT_EVENT_SIZE: usize = std::mem::size_of::<uapi::input_event>(); |
| |
| async fn start_touch_input( |
| locked: &mut Locked<Unlocked>, |
| current_task: &CurrentTask, |
| ) -> (InputDevice, FileHandle, fuipointer::TouchSourceRequestStream) { |
| let inspector = fuchsia_inspect::Inspector::default(); |
| start_touch_input_inspect_and_dimensions(locked, current_task, 700, 1200, &inspector).await |
| } |
| |
| async fn start_touch_input_inspect( |
| locked: &mut Locked<Unlocked>, |
| current_task: &CurrentTask, |
| inspector: &fuchsia_inspect::Inspector, |
| ) -> (InputDevice, FileHandle, fuipointer::TouchSourceRequestStream) { |
| start_touch_input_inspect_and_dimensions(locked, current_task, 700, 1200, &inspector).await |
| } |
| |
| async fn init_keyboard_listener( |
| keyboard_stream: &mut fuiinput::KeyboardRequestStream, |
| ) -> fuiinput::KeyboardListenerProxy { |
| let keyboard_listener = match keyboard_stream.next().await { |
| Some(Ok(fuiinput::KeyboardRequest::AddListener { |
| view_ref: _, |
| listener, |
| responder, |
| })) => { |
| let _ = responder.send(); |
| listener.into_proxy() |
| } |
| _ => { |
| panic!("Failed to get event"); |
| } |
| }; |
| |
| keyboard_listener |
| } |
| |
| async fn init_button_listeners( |
| device_listener_stream: &mut fuipolicy::DeviceListenerRegistryRequestStream, |
| ) -> (fuipolicy::MediaButtonsListenerProxy, fuipolicy::TouchButtonsListenerProxy) { |
| let media_buttons_listener = match device_listener_stream.next().await { |
| Some(Ok(fuipolicy::DeviceListenerRegistryRequest::RegisterListener { |
| listener, |
| responder, |
| })) => { |
| let _ = responder.send(); |
| listener.into_proxy() |
| } |
| _ => { |
| panic!("Failed to get event"); |
| } |
| }; |
| |
| let touch_buttons_listener = match device_listener_stream.next().await { |
| Some(Ok(fuipolicy::DeviceListenerRegistryRequest::RegisterTouchButtonsListener { |
| listener, |
| responder, |
| })) => { |
| let _ = responder.send(); |
| listener.into_proxy() |
| } |
| _ => { |
| panic!("Failed to get event"); |
| } |
| }; |
| |
| (media_buttons_listener, touch_buttons_listener) |
| } |
| |
| async fn start_touch_input_inspect_and_dimensions( |
| locked: &mut Locked<Unlocked>, |
| current_task: &CurrentTask, |
| x_max: i32, |
| y_max: i32, |
| inspector: &fuchsia_inspect::Inspector, |
| ) -> (InputDevice, FileHandle, fuipointer::TouchSourceRequestStream) { |
| let input_device = InputDevice::new_touch(x_max, y_max, inspector.root()); |
| let input_file = |
| input_device.open_test(locked, current_task).expect("Failed to create input file"); |
| |
| let (touch_source_client_end, touch_source_stream) = |
| fidl::endpoints::create_request_stream::<TouchSourceMarker>(); |
| |
| let (mouse_source_client_end, _mouse_source_stream) = |
| fidl::endpoints::create_request_stream::<fuipointer::MouseSourceMarker>(); |
| |
| let (keyboard_proxy, mut keyboard_stream) = |
| fidl::endpoints::create_sync_proxy_and_stream::<fuiinput::KeyboardMarker>(); |
| let view_ref_pair = |
| fuchsia_scenic::ViewRefPair::new().expect("Failed to create ViewRefPair"); |
| |
| let (device_registry_proxy, mut device_listener_stream) = |
| fidl::endpoints::create_sync_proxy_and_stream::<fuipolicy::DeviceListenerRegistryMarker>( |
| ); |
| |
| let (relay, _relay_handle) = input_event_relay::new_input_relay(); |
| relay.start_relays( |
| ¤t_task.kernel(), |
| EventProxyMode::None, |
| touch_source_client_end, |
| keyboard_proxy, |
| mouse_source_client_end, |
| view_ref_pair.view_ref, |
| device_registry_proxy, |
| input_device.open_files.clone(), |
| Default::default(), |
| Default::default(), |
| Some(input_device.inspect_status.clone()), |
| None, |
| None, |
| ); |
| |
| let _ = init_keyboard_listener(&mut keyboard_stream).await; |
| let _ = init_button_listeners(&mut device_listener_stream).await; |
| |
| (input_device, input_file, touch_source_stream) |
| } |
| |
| async fn start_keyboard_input( |
| locked: &mut Locked<Unlocked>, |
| current_task: &CurrentTask, |
| ) -> (InputDevice, FileHandle, fuiinput::KeyboardListenerProxy) { |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let input_device = InputDevice::new_keyboard(inspector.root()); |
| let input_file = |
| input_device.open_test(locked, current_task).expect("Failed to create input file"); |
| let (keyboard_proxy, mut keyboard_stream) = |
| fidl::endpoints::create_sync_proxy_and_stream::<fuiinput::KeyboardMarker>(); |
| let view_ref_pair = |
| fuchsia_scenic::ViewRefPair::new().expect("Failed to create ViewRefPair"); |
| |
| let (device_registry_proxy, mut device_listener_stream) = |
| fidl::endpoints::create_sync_proxy_and_stream::<fuipolicy::DeviceListenerRegistryMarker>( |
| ); |
| |
| let (touch_source_client_end, _touch_source_stream) = |
| fidl::endpoints::create_request_stream::<TouchSourceMarker>(); |
| |
| let (mouse_source_client_end, _mouse_source_stream) = |
| fidl::endpoints::create_request_stream::<fuipointer::MouseSourceMarker>(); |
| |
| let (relay, _relay_handle) = input_event_relay::new_input_relay(); |
| relay.start_relays( |
| current_task.kernel(), |
| EventProxyMode::None, |
| touch_source_client_end, |
| keyboard_proxy, |
| mouse_source_client_end, |
| view_ref_pair.view_ref, |
| device_registry_proxy, |
| Default::default(), |
| input_device.open_files.clone(), |
| Default::default(), |
| None, |
| Some(input_device.inspect_status.clone()), |
| None, |
| ); |
| |
| let keyboad_listener = init_keyboard_listener(&mut keyboard_stream).await; |
| let _ = init_button_listeners(&mut device_listener_stream).await; |
| |
| (input_device, input_file, keyboad_listener) |
| } |
| |
| async fn start_button_input( |
| locked: &mut Locked<Unlocked>, |
| current_task: &CurrentTask, |
| ) -> (InputDevice, FileHandle, fuipolicy::MediaButtonsListenerProxy) { |
| let inspector = fuchsia_inspect::Inspector::default(); |
| start_button_input_inspect(locked, current_task, &inspector).await |
| } |
| |
| async fn start_button_input_inspect( |
| locked: &mut Locked<Unlocked>, |
| current_task: &CurrentTask, |
| inspector: &fuchsia_inspect::Inspector, |
| ) -> (InputDevice, FileHandle, fuipolicy::MediaButtonsListenerProxy) { |
| let input_device = InputDevice::new_keyboard(inspector.root()); |
| let input_file = |
| input_device.open_test(locked, current_task).expect("Failed to create input file"); |
| let (device_registry_proxy, mut device_listener_stream) = |
| fidl::endpoints::create_sync_proxy_and_stream::<fuipolicy::DeviceListenerRegistryMarker>( |
| ); |
| |
| let (touch_source_client_end, _touch_source_stream) = |
| fidl::endpoints::create_request_stream::<TouchSourceMarker>(); |
| let (mouse_source_client_end, _mouse_source_stream) = |
| fidl::endpoints::create_request_stream::<fuipointer::MouseSourceMarker>(); |
| let (keyboard_proxy, mut keyboard_stream) = |
| fidl::endpoints::create_sync_proxy_and_stream::<fuiinput::KeyboardMarker>(); |
| let view_ref_pair = |
| fuchsia_scenic::ViewRefPair::new().expect("Failed to create ViewRefPair"); |
| |
| let (relay, _relay_handle) = input_event_relay::new_input_relay(); |
| relay.start_relays( |
| current_task.kernel(), |
| EventProxyMode::None, |
| touch_source_client_end, |
| keyboard_proxy, |
| mouse_source_client_end, |
| view_ref_pair.view_ref, |
| device_registry_proxy, |
| Default::default(), |
| input_device.open_files.clone(), |
| Default::default(), |
| None, |
| Some(input_device.inspect_status.clone()), |
| None, |
| ); |
| |
| let _ = init_keyboard_listener(&mut keyboard_stream).await; |
| let (button_listener, _) = init_button_listeners(&mut device_listener_stream).await; |
| |
| (input_device, input_file, button_listener) |
| } |
| |
| async fn start_mouse_input( |
| locked: &mut Locked<Unlocked>, |
| current_task: &CurrentTask, |
| ) -> (InputDevice, FileHandle, fuipointer::MouseSourceRequestStream) { |
| let inspector = fuchsia_inspect::Inspector::default(); |
| start_mouse_input_inspect(locked, current_task, &inspector).await |
| } |
| |
| async fn start_mouse_input_inspect( |
| locked: &mut Locked<Unlocked>, |
| current_task: &CurrentTask, |
| inspector: &fuchsia_inspect::Inspector, |
| ) -> (InputDevice, FileHandle, fuipointer::MouseSourceRequestStream) { |
| let input_device = InputDevice::new_mouse(inspector.root()); |
| let input_file = |
| input_device.open_test(locked, current_task).expect("Failed to create input file"); |
| |
| let (touch_source_client_end, _touch_source_stream) = |
| fidl::endpoints::create_request_stream::<TouchSourceMarker>(); |
| |
| let (mouse_source_client_end, mouse_source_stream) = |
| fidl::endpoints::create_request_stream::<fuipointer::MouseSourceMarker>(); |
| |
| let (keyboard_proxy, mut keyboard_stream) = |
| fidl::endpoints::create_sync_proxy_and_stream::<fuiinput::KeyboardMarker>(); |
| let view_ref_pair = |
| fuchsia_scenic::ViewRefPair::new().expect("Failed to create ViewRefPair"); |
| |
| let (device_registry_proxy, mut device_listener_stream) = |
| fidl::endpoints::create_sync_proxy_and_stream::<fuipolicy::DeviceListenerRegistryMarker>( |
| ); |
| |
| let (relay, _relay_handle) = input_event_relay::new_input_relay(); |
| relay.start_relays( |
| ¤t_task.kernel(), |
| EventProxyMode::None, |
| touch_source_client_end, |
| keyboard_proxy, |
| mouse_source_client_end, |
| view_ref_pair.view_ref, |
| device_registry_proxy, |
| Default::default(), |
| Default::default(), |
| input_device.open_files.clone(), |
| None, |
| None, |
| Some(input_device.inspect_status.clone()), |
| ); |
| |
| let _ = init_keyboard_listener(&mut keyboard_stream).await; |
| let _ = init_button_listeners(&mut device_listener_stream).await; |
| |
| (input_device, input_file, mouse_source_stream) |
| } |
| |
| fn make_touch_event(pointer_id: u32) -> fuipointer::TouchEvent { |
| // Default to `Change`, because that has the fewest side effects. |
| make_touch_event_with_phase(EventPhase::Change, pointer_id) |
| } |
| |
| fn make_touch_event_with_phase(phase: EventPhase, pointer_id: u32) -> fuipointer::TouchEvent { |
| make_touch_event_with_coords_phase(0.0, 0.0, phase, pointer_id) |
| } |
| |
| fn make_touch_event_with_coords_phase( |
| x: f32, |
| y: f32, |
| phase: EventPhase, |
| pointer_id: u32, |
| ) -> fuipointer::TouchEvent { |
| make_touch_event_with_coords_phase_timestamp(x, y, phase, pointer_id, 0) |
| } |
| |
| fn make_touch_event_with_coords(x: f32, y: f32, pointer_id: u32) -> fuipointer::TouchEvent { |
| make_touch_event_with_coords_phase(x, y, EventPhase::Change, pointer_id) |
| } |
| |
| fn make_touch_event_with_coords_phase_timestamp( |
| x: f32, |
| y: f32, |
| phase: EventPhase, |
| pointer_id: u32, |
| time_nanos: i64, |
| ) -> fuipointer::TouchEvent { |
| make_touch_event_with_coords_phase_timestamp_device_id( |
| x, y, phase, pointer_id, time_nanos, 0, |
| ) |
| } |
| |
| fn make_empty_touch_event() -> fuipointer::TouchEvent { |
| TouchEvent { |
| pointer_sample: Some(TouchPointerSample { |
| interaction: Some(TouchInteractionId { |
| pointer_id: 0, |
| device_id: 0, |
| interaction_id: 0, |
| }), |
| ..Default::default() |
| }), |
| ..Default::default() |
| } |
| } |
| |
| fn make_touch_event_with_coords_phase_timestamp_device_id( |
| x: f32, |
| y: f32, |
| phase: EventPhase, |
| pointer_id: u32, |
| time_nanos: i64, |
| device_id: u32, |
| ) -> fuipointer::TouchEvent { |
| TouchEvent { |
| timestamp: Some(time_nanos), |
| pointer_sample: Some(TouchPointerSample { |
| position_in_viewport: Some([x, y]), |
| // Default to `Change`, because that has the fewest side effects. |
| phase: Some(phase), |
| interaction: Some(TouchInteractionId { pointer_id, device_id, interaction_id: 0 }), |
| ..Default::default() |
| }), |
| ..Default::default() |
| } |
| } |
| |
| fn make_mouse_wheel_event(ticks: i64) -> fuipointer::MouseEvent { |
| make_mouse_wheel_event_with_timestamp(ticks, 0) |
| } |
| |
| fn make_mouse_wheel_event_with_timestamp(ticks: i64, timestamp: i64) -> fuipointer::MouseEvent { |
| fuipointer::MouseEvent { |
| timestamp: Some(timestamp), |
| pointer_sample: Some(fuipointer::MousePointerSample { |
| device_id: Some(0), |
| scroll_v: Some(ticks), |
| ..Default::default() |
| }), |
| ..Default::default() |
| } |
| } |
| |
| fn read_uapi_events<L>( |
| locked: &mut Locked<L>, |
| file: &FileHandle, |
| current_task: &CurrentTask, |
| ) -> Vec<uapi::input_event> |
| where |
| L: LockEqualOrBefore<FileOpsCore>, |
| { |
| std::iter::from_fn(|| { |
| let locked = locked.cast_locked::<FileOpsCore>(); |
| let mut event_bytes = VecOutputBuffer::new(INPUT_EVENT_SIZE); |
| match file.read(locked, current_task, &mut event_bytes) { |
| Ok(INPUT_EVENT_SIZE) => Some( |
| uapi::input_event::read_from_bytes(Vec::from(event_bytes).as_slice()) |
| .map_err(|_| anyhow!("failed to read input_event from buffer")), |
| ), |
| Ok(other_size) => { |
| Some(Err(anyhow!("got {} bytes (expected {})", other_size, INPUT_EVENT_SIZE))) |
| } |
| Err(Errno { code: EAGAIN, .. }) => None, |
| Err(other_error) => Some(Err(anyhow!("read failed: {:?}", other_error))), |
| } |
| }) |
| .enumerate() |
| .map(|(i, read_res)| match read_res { |
| Ok(event) => event, |
| Err(e) => panic!("unexpected result {:?} on iteration {}", e, i), |
| }) |
| .collect() |
| } |
| |
| // Waits for a `Watch()` request to arrive on `request_stream`, and responds with |
| // `touch_event`. Returns the arguments to the `Watch()` call. |
| async fn answer_next_touch_watch_request( |
| request_stream: &mut fuipointer::TouchSourceRequestStream, |
| touch_events: Vec<TouchEvent>, |
| ) -> Vec<TouchResponse> { |
| match request_stream.next().await { |
| Some(Ok(TouchSourceRequest::Watch { responses, responder })) => { |
| responder.send(&touch_events).expect("failure sending Watch reply"); |
| responses |
| } |
| unexpected_request => panic!("unexpected request {:?}", unexpected_request), |
| } |
| } |
| |
| // Waits for a `Watch()` request to arrive on `request_stream`, and responds with |
| // `mouse_events`. |
| async fn answer_next_mouse_watch_request( |
| request_stream: &mut fuipointer::MouseSourceRequestStream, |
| mouse_events: Vec<fuipointer::MouseEvent>, |
| ) { |
| match request_stream.next().await { |
| Some(Ok(fuipointer::MouseSourceRequest::Watch { responder })) => { |
| responder.send(&mouse_events).expect("failure sending Watch reply"); |
| } |
| unexpected_request => panic!("unexpected request {:?}", unexpected_request), |
| } |
| } |
| |
| #[::fuchsia::test()] |
| async fn initial_watch_request_has_empty_responses_arg() { |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| // Set up resources. |
| let (_input_device, _input_file, mut touch_source_stream) = |
| start_touch_input(locked, ¤t_task).await; |
| |
| // Verify that the watch request has empty `responses`. |
| assert_matches!( |
| touch_source_stream.next().await, |
| Some(Ok(TouchSourceRequest::Watch { responses, .. })) |
| => assert_eq!(responses.as_slice(), []) |
| ); |
| } |
| |
| #[::fuchsia::test] |
| async fn later_watch_requests_have_responses_arg_matching_earlier_watch_replies() { |
| // Set up resources. |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, _input_file, mut touch_source_stream) = |
| start_touch_input(locked, ¤t_task).await; |
| |
| // Reply to first `Watch` with two `TouchEvent`s. |
| match touch_source_stream.next().await { |
| Some(Ok(TouchSourceRequest::Watch { responder, .. })) => responder |
| .send(&vec![TouchEvent::default(); 2]) |
| .expect("failure sending Watch reply"), |
| unexpected_request => panic!("unexpected request {:?}", unexpected_request), |
| } |
| |
| // Verify second `Watch` has two elements in `responses`. |
| // Then reply with five `TouchEvent`s. |
| match touch_source_stream.next().await { |
| Some(Ok(TouchSourceRequest::Watch { responses, responder })) => { |
| assert_matches!(responses.as_slice(), [_, _]); |
| responder |
| .send(&vec![TouchEvent::default(); 5]) |
| .expect("failure sending Watch reply") |
| } |
| unexpected_request => panic!("unexpected request {:?}", unexpected_request), |
| } |
| |
| // Verify third `Watch` has five elements in `responses`. |
| match touch_source_stream.next().await { |
| Some(Ok(TouchSourceRequest::Watch { responses, .. })) => { |
| assert_matches!(responses.as_slice(), [_, _, _, _, _]); |
| } |
| unexpected_request => panic!("unexpected request {:?}", unexpected_request), |
| } |
| } |
| |
| #[::fuchsia::test] |
| async fn notifies_polling_waiters_of_new_data() { |
| // Set up resources. |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, mut touch_source_stream) = |
| start_touch_input(locked, ¤t_task).await; |
| let waiter1 = Waiter::new(); |
| let waiter2 = Waiter::new(); |
| |
| // Ask `input_file` to notify waiters when data is available to read. |
| [&waiter1, &waiter2].iter().for_each(|waiter| { |
| input_file.wait_async( |
| locked, |
| ¤t_task, |
| waiter, |
| FdEvents::POLLIN, |
| EventHandler::None, |
| ); |
| }); |
| assert_matches!( |
| waiter1.wait_until(locked, ¤t_task, zx::MonotonicInstant::ZERO), |
| Err(_) |
| ); |
| assert_matches!( |
| waiter2.wait_until(locked, ¤t_task, zx::MonotonicInstant::ZERO), |
| Err(_) |
| ); |
| |
| // Reply to first `Watch` request. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![make_touch_event_with_phase(EventPhase::Add, 1)], |
| ) |
| .await; |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![make_touch_event(1)]).await; |
| |
| // `InputFile` should be done processing the first reply, since it has sent its second |
| // request. And, as part of processing the first reply, `InputFile` should have notified |
| // the interested waiters. |
| assert_eq!(waiter1.wait_until(locked, ¤t_task, zx::MonotonicInstant::ZERO), Ok(())); |
| assert_eq!(waiter2.wait_until(locked, ¤t_task, zx::MonotonicInstant::ZERO), Ok(())); |
| } |
| |
| #[::fuchsia::test] |
| async fn notifies_blocked_waiter_of_new_data() { |
| // Set up resources. |
| let (kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, mut touch_source_stream) = |
| start_touch_input(locked, ¤t_task).await; |
| let waiter = Waiter::new(); |
| |
| // Ask `input_file` to notify `waiter` when data is available to read. |
| input_file.wait_async(locked, ¤t_task, &waiter, FdEvents::POLLIN, EventHandler::None); |
| |
| let mut waiter_thread = kernel |
| .kthreads |
| .spawner() |
| .spawn_and_get_result(move |locked, task| waiter.wait(locked, &task)); |
| assert!(futures::poll!(&mut waiter_thread).is_pending()); |
| |
| // Reply to first `Watch` request. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![make_touch_event_with_phase(EventPhase::Add, 1)], |
| ) |
| .await; |
| |
| // Wait for another `Watch`. |
| // |
| // TODO(https://fxbug.dev/42075452): Without this, `relay_thread` gets stuck `await`-ing |
| // the reply to its first request. Figure out why that happens, and remove this second |
| // reply. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![make_touch_event(1)]).await; |
| } |
| |
| #[::fuchsia::test] |
| async fn does_not_notify_polling_waiters_without_new_data() { |
| // Set up resources. |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, mut touch_source_stream) = |
| start_touch_input(locked, ¤t_task).await; |
| let waiter1 = Waiter::new(); |
| let waiter2 = Waiter::new(); |
| |
| // Ask `input_file` to notify waiters when data is available to read. |
| [&waiter1, &waiter2].iter().for_each(|waiter| { |
| input_file.wait_async( |
| locked, |
| ¤t_task, |
| waiter, |
| FdEvents::POLLIN, |
| EventHandler::None, |
| ); |
| }); |
| assert_matches!( |
| waiter1.wait_until(locked, ¤t_task, zx::MonotonicInstant::ZERO), |
| Err(_) |
| ); |
| assert_matches!( |
| waiter2.wait_until(locked, ¤t_task, zx::MonotonicInstant::ZERO), |
| Err(_) |
| ); |
| |
| // Reply to first `Watch` request with an empty set of events. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![]).await; |
| |
| // `InputFile` should be done processing the first reply. Since there |
| // were no touch_events given, `InputFile` should not have notified the |
| // interested waiters. |
| assert_matches!( |
| waiter1.wait_until(locked, ¤t_task, zx::MonotonicInstant::ZERO), |
| Err(_) |
| ); |
| assert_matches!( |
| waiter2.wait_until(locked, ¤t_task, zx::MonotonicInstant::ZERO), |
| Err(_) |
| ); |
| } |
| |
| // Note: a user program may also want to be woken if events were already ready at the |
| // time that the program called `epoll_wait()`. However, there's no test for that case |
| // in this module, because: |
| // |
| // 1. Not all programs will want to be woken in such a case. In particular, some programs |
| // use "edge-triggered" mode instead of "level-tiggered" mode. For details on the |
| // two modes, see https://man7.org/linux/man-pages/man7/epoll.7.html. |
| // 2. For programs using "level-triggered" mode, the relevant behavior is implemented in |
| // the `epoll` module, and verified by `epoll::tests::test_epoll_ready_then_wait()`. |
| // |
| // See also: the documentation for `FileOps::wait_async()`. |
| |
| #[::fuchsia::test] |
| async fn honors_wait_cancellation() { |
| // Set up input resources. |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, mut touch_source_stream) = |
| start_touch_input(locked, ¤t_task).await; |
| let waiter1 = Waiter::new(); |
| let waiter2 = Waiter::new(); |
| |
| // Ask `input_file` to notify `waiter` when data is available to read. |
| let waitkeys = [&waiter1, &waiter2] |
| .iter() |
| .map(|waiter| { |
| input_file |
| .wait_async(locked, ¤t_task, waiter, FdEvents::POLLIN, EventHandler::None) |
| .expect("wait_async") |
| }) |
| .collect::<Vec<_>>(); |
| |
| // Cancel wait for `waiter1`. |
| waitkeys.into_iter().next().expect("failed to get first waitkey").cancel(); |
| |
| // Reply to first `Watch` request. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![make_touch_event_with_phase(EventPhase::Add, 1)], |
| ) |
| .await; |
| // Wait for another `Watch`. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![make_touch_event(1)]).await; |
| |
| // `InputFile` should be done processing the first reply, since it has sent its second |
| // request. And, as part of processing the first reply, `InputFile` should have notified |
| // the interested waiters. |
| assert_matches!( |
| waiter1.wait_until(locked, ¤t_task, zx::MonotonicInstant::ZERO), |
| Err(_) |
| ); |
| assert_eq!(waiter2.wait_until(locked, ¤t_task, zx::MonotonicInstant::ZERO), Ok(())); |
| } |
| |
| #[::fuchsia::test] |
| async fn query_events() { |
| // Set up resources. |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, mut touch_source_stream) = |
| start_touch_input(locked, ¤t_task).await; |
| |
| // Check initial expectation. |
| assert_eq!( |
| input_file.query_events(locked, ¤t_task).expect("query_events"), |
| FdEvents::empty(), |
| "events should be empty before data arrives" |
| ); |
| |
| // Reply to first `Watch` request. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![make_touch_event_with_phase(EventPhase::Add, 1)], |
| ) |
| .await; |
| |
| // Wait for another `Watch`. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![make_touch_event(1)]).await; |
| |
| // Check post-watch expectation. |
| assert_eq!( |
| input_file.query_events(locked, ¤t_task).expect("query_events"), |
| FdEvents::POLLIN | FdEvents::POLLRDNORM, |
| "events should be POLLIN after data arrives" |
| ); |
| } |
| |
| fn make_uapi_input_event(ty: u32, code: u32, value: i32) -> uapi::input_event { |
| make_uapi_input_event_with_timestamp(ty, code, value, 0) |
| } |
| |
| fn make_uapi_input_event_with_timestamp( |
| ty: u32, |
| code: u32, |
| value: i32, |
| time_nanos: i64, |
| ) -> uapi::input_event { |
| uapi::input_event { |
| time: timeval_from_time(zx::MonotonicInstant::from_nanos(time_nanos)), |
| type_: ty as u16, |
| code: code as u16, |
| value, |
| } |
| } |
| |
| #[::fuchsia::test] |
| async fn touch_event_ignored() { |
| // Set up resources. |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, mut touch_source_stream) = |
| start_touch_input_inspect(locked, ¤t_task, &inspector).await; |
| |
| // Touch add for pointer 1. This should be counted as a received event and a converted |
| // event. It should also yield 6 generated events. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![make_touch_event_with_phase(EventPhase::Add, 1)], |
| ) |
| .await; |
| |
| // Wait for another `Watch` to ensure input_file done processing the first reply. |
| // Use an empty `TouchEvent`, to minimize the chance that this event creates unexpected |
| // `uapi::input_event`s. This should be counted as a received event and an ignored event. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![make_empty_touch_event()]) |
| .await; |
| |
| // Consume all of the `uapi::input_event`s that are available. |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| |
| assert_eq!(events.len(), 6); |
| |
| // Reply to `Watch` request of empty event. This should be counted as a received event and |
| // an ignored event. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![make_empty_touch_event()]) |
| .await; |
| |
| // Wait for another `Watch`. |
| match touch_source_stream.next().await { |
| Some(Ok(TouchSourceRequest::Watch { responses, .. })) => { |
| assert_matches!(responses.as_slice(), [_]) |
| } |
| unexpected_request => panic!("unexpected request {:?}", unexpected_request), |
| } |
| |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| assert_eq!(events, vec![]); |
| assert_data_tree!(inspector, root: { |
| touch_device: { |
| total_fidl_events_received_count: 3u64, |
| total_fidl_events_ignored_count: 2u64, |
| total_fidl_events_unexpected_count: 0u64, |
| total_fidl_events_converted_count: 1u64, |
| total_uapi_events_generated_count: 6u64, |
| last_generated_uapi_event_timestamp_ns: 0i64, |
| touch_file_0: { |
| fidl_events_received_count: 3u64, |
| fidl_events_ignored_count: 2u64, |
| fidl_events_unexpected_count: 0u64, |
| fidl_events_converted_count: 1u64, |
| uapi_events_generated_count: 6u64, |
| uapi_events_read_count: 6u64, |
| last_generated_uapi_event_timestamp_ns: 0i64, |
| last_read_uapi_event_timestamp_ns: 0i64, |
| }, |
| } |
| }); |
| } |
| |
| #[test_case(make_touch_event_with_phase(EventPhase::Add, 1); "touch add for pointer already added")] |
| #[test_case(make_touch_event_with_phase(EventPhase::Change, 2); "touch change for pointer not added")] |
| #[test_case(make_touch_event_with_phase(EventPhase::Remove, 2); "touch remove for pointer not added")] |
| #[test_case(make_touch_event_with_phase(EventPhase::Cancel, 1); "touch cancel")] |
| #[::fuchsia::test] |
| async fn touch_event_unexpected(event: TouchEvent) { |
| // Set up resources. |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, mut touch_source_stream) = |
| start_touch_input_inspect(locked, ¤t_task, &inspector).await; |
| |
| // Touch add for pointer 1. This should be counted as a received event and a converted |
| // event. It should also yield 6 generated events. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![make_touch_event_with_phase(EventPhase::Add, 1)], |
| ) |
| .await; |
| |
| // Wait for another `Watch` to ensure input_file done processing the first reply. |
| // Use an empty `TouchEvent`, to minimize the chance that this event creates unexpected |
| // `uapi::input_event`s. This should be counted as a received event and an ignored event. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![make_empty_touch_event()]) |
| .await; |
| |
| // Consume all of the `uapi::input_event`s that are available. |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| |
| assert_eq!(events.len(), 6); |
| |
| // Reply to `Watch` request of given event. This should be counted as a received event and |
| // an unexpected event. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![event]).await; |
| |
| // Wait for another `Watch`. |
| match touch_source_stream.next().await { |
| Some(Ok(TouchSourceRequest::Watch { responses, .. })) => { |
| assert_matches!(responses.as_slice(), [_]) |
| } |
| unexpected_request => panic!("unexpected request {:?}", unexpected_request), |
| } |
| |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| assert_eq!(events, vec![]); |
| assert_data_tree!(inspector, root: { |
| touch_device: { |
| total_fidl_events_received_count: 3u64, |
| total_fidl_events_ignored_count: 1u64, |
| total_fidl_events_unexpected_count: 1u64, |
| total_fidl_events_converted_count: 1u64, |
| total_uapi_events_generated_count: 6u64, |
| last_generated_uapi_event_timestamp_ns: 0i64, |
| touch_file_0: { |
| fidl_events_received_count: 3u64, |
| fidl_events_ignored_count: 1u64, |
| fidl_events_unexpected_count: 1u64, |
| fidl_events_converted_count: 1u64, |
| uapi_events_generated_count: 6u64, |
| uapi_events_read_count: 6u64, |
| last_generated_uapi_event_timestamp_ns: 0i64, |
| last_read_uapi_event_timestamp_ns: 0i64, |
| }, |
| } |
| }); |
| } |
| |
| #[::fuchsia::test] |
| async fn translates_touch_add() { |
| // Set up resources. |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, mut touch_source_stream) = |
| start_touch_input(locked, ¤t_task).await; |
| |
| // Touch add for pointer 1. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![make_touch_event_with_phase(EventPhase::Add, 1)], |
| ) |
| .await; |
| |
| // Wait for another `Watch` to ensure input_file done processing the first reply. |
| // Use an empty `TouchEvent`, to minimize the chance that this event |
| // creates unexpected `uapi::input_event`s. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![make_empty_touch_event()]) |
| .await; |
| |
| // Consume all of the `uapi::input_event`s that are available. |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| |
| assert_eq!( |
| events, |
| vec![ |
| make_uapi_input_event(uapi::EV_KEY, uapi::BTN_TOUCH, 1), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_SLOT, 0), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_TRACKING_ID, 1), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_POSITION_X, 0), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_POSITION_Y, 0), |
| make_uapi_input_event(uapi::EV_SYN, uapi::SYN_REPORT, 0), |
| ] |
| ); |
| } |
| |
| #[::fuchsia::test] |
| async fn translates_touch_change() { |
| // Set up resources. |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, mut touch_source_stream) = |
| start_touch_input(locked, ¤t_task).await; |
| |
| // Touch add for pointer 1. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![make_touch_event_with_phase(EventPhase::Add, 1)], |
| ) |
| .await; |
| |
| // Wait for another `Watch` to ensure input_file done processing the first reply. |
| // Use an empty `TouchEvent`, to minimize the chance that this event |
| // creates unexpected `uapi::input_event`s. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![TouchEvent::default()]) |
| .await; |
| |
| // Consume all of the `uapi::input_event`s that are available. |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| |
| assert_eq!(events.len(), 6); |
| |
| // Reply to touch change. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![make_touch_event_with_coords(10.0, 20.0, 1)], |
| ) |
| .await; |
| |
| // Wait for another `Watch`. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![TouchEvent::default()]) |
| .await; |
| |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| assert_eq!( |
| events, |
| vec![ |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_SLOT, 0), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_POSITION_X, 10), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_POSITION_Y, 20), |
| make_uapi_input_event(uapi::EV_SYN, uapi::SYN_REPORT, 0), |
| ] |
| ); |
| } |
| |
| #[::fuchsia::test] |
| async fn translates_touch_remove() { |
| // Set up resources. |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, mut touch_source_stream) = |
| start_touch_input(locked, ¤t_task).await; |
| |
| // Touch add for pointer 1. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![make_touch_event_with_phase(EventPhase::Add, 1)], |
| ) |
| .await; |
| |
| // Wait for another `Watch` to ensure input_file done processing the first reply. |
| // Use an empty `TouchEvent`, to minimize the chance that this event |
| // creates unexpected `uapi::input_event`s. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![TouchEvent::default()]) |
| .await; |
| |
| // Consume all of the `uapi::input_event`s that are available. |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| |
| assert_eq!(events.len(), 6); |
| |
| // Reply to touch change. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![make_touch_event_with_phase(EventPhase::Remove, 1)], |
| ) |
| .await; |
| |
| // Wait for another `Watch`. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![TouchEvent::default()]) |
| .await; |
| |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| assert_eq!( |
| events, |
| vec![ |
| make_uapi_input_event(uapi::EV_KEY, uapi::BTN_TOUCH, 0), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_SLOT, 0), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_TRACKING_ID, -1), |
| make_uapi_input_event(uapi::EV_SYN, uapi::SYN_REPORT, 0), |
| ] |
| ); |
| } |
| |
| #[::fuchsia::test] |
| async fn multi_touch_event_sequence() { |
| // Set up resources. |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, mut touch_source_stream) = |
| start_touch_input(locked, ¤t_task).await; |
| |
| // Touch add for pointer 1. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![make_touch_event_with_phase(EventPhase::Add, 1)], |
| ) |
| .await; |
| |
| // Wait for another `Watch`. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![TouchEvent::default()]) |
| .await; |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| |
| assert_eq!(events.len(), 6); |
| |
| // Touch add for pointer 2. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![ |
| make_touch_event_with_coords(10.0, 20.0, 1), |
| make_touch_event_with_phase(EventPhase::Add, 2), |
| ], |
| ) |
| .await; |
| |
| // Wait for another `Watch`. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![TouchEvent::default()]) |
| .await; |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| |
| assert_eq!( |
| events, |
| vec![ |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_SLOT, 0), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_POSITION_X, 10), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_POSITION_Y, 20), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_SLOT, 1), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_TRACKING_ID, 2), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_POSITION_X, 0), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_POSITION_Y, 0), |
| make_uapi_input_event(uapi::EV_SYN, uapi::SYN_REPORT, 0), |
| ] |
| ); |
| |
| // Both pointers move. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![ |
| make_touch_event_with_coords(11.0, 21.0, 1), |
| make_touch_event_with_coords(101.0, 201.0, 2), |
| ], |
| ) |
| .await; |
| |
| // Wait for another `Watch`. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![TouchEvent::default()]) |
| .await; |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| |
| assert_eq!( |
| events, |
| vec![ |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_SLOT, 0), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_POSITION_X, 11), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_POSITION_Y, 21), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_SLOT, 1), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_POSITION_X, 101), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_POSITION_Y, 201), |
| make_uapi_input_event(uapi::EV_SYN, uapi::SYN_REPORT, 0), |
| ] |
| ); |
| |
| // Pointer 1 up. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![ |
| make_touch_event_with_phase(EventPhase::Remove, 1), |
| make_touch_event_with_coords(101.0, 201.0, 2), |
| ], |
| ) |
| .await; |
| |
| // Wait for another `Watch`. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![TouchEvent::default()]) |
| .await; |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| |
| assert_eq!( |
| events, |
| vec![ |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_SLOT, 0), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_TRACKING_ID, -1), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_SLOT, 1), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_POSITION_X, 101), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_POSITION_Y, 201), |
| make_uapi_input_event(uapi::EV_SYN, uapi::SYN_REPORT, 0), |
| ] |
| ); |
| |
| // Pointer 2 up. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![make_touch_event_with_phase(EventPhase::Remove, 2)], |
| ) |
| .await; |
| |
| // Wait for another `Watch`. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![TouchEvent::default()]) |
| .await; |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| |
| assert_eq!( |
| events, |
| vec![ |
| make_uapi_input_event(uapi::EV_KEY, uapi::BTN_TOUCH, 0), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_SLOT, 1), |
| make_uapi_input_event(uapi::EV_ABS, uapi::ABS_MT_TRACKING_ID, -1), |
| make_uapi_input_event(uapi::EV_SYN, uapi::SYN_REPORT, 0), |
| ] |
| ); |
| } |
| |
| #[::fuchsia::test] |
| async fn multi_event_sequence_unsorted_in_one_watch() { |
| // Set up resources. |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, mut touch_source_stream) = |
| start_touch_input(locked, ¤t_task).await; |
| |
| // Touch add for pointer 1. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![ |
| make_touch_event_with_coords_phase_timestamp( |
| 10.0, |
| 20.0, |
| EventPhase::Change, |
| 1, |
| 100, |
| ), |
| make_touch_event_with_coords_phase_timestamp(0.0, 0.0, EventPhase::Add, 1, 1), |
| ], |
| ) |
| .await; |
| |
| // Wait for another `Watch`. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![TouchEvent::default()]) |
| .await; |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| |
| assert_eq!( |
| events, |
| vec![ |
| make_uapi_input_event_with_timestamp(uapi::EV_KEY, uapi::BTN_TOUCH, 1, 1), |
| make_uapi_input_event_with_timestamp(uapi::EV_ABS, uapi::ABS_MT_SLOT, 0, 1), |
| make_uapi_input_event_with_timestamp(uapi::EV_ABS, uapi::ABS_MT_TRACKING_ID, 1, 1), |
| make_uapi_input_event_with_timestamp(uapi::EV_ABS, uapi::ABS_MT_POSITION_X, 0, 1), |
| make_uapi_input_event_with_timestamp(uapi::EV_ABS, uapi::ABS_MT_POSITION_Y, 0, 1), |
| make_uapi_input_event_with_timestamp(uapi::EV_SYN, uapi::SYN_REPORT, 0, 1), |
| make_uapi_input_event_with_timestamp(uapi::EV_ABS, uapi::ABS_MT_SLOT, 0, 100), |
| make_uapi_input_event_with_timestamp( |
| uapi::EV_ABS, |
| uapi::ABS_MT_POSITION_X, |
| 10, |
| 100 |
| ), |
| make_uapi_input_event_with_timestamp( |
| uapi::EV_ABS, |
| uapi::ABS_MT_POSITION_Y, |
| 20, |
| 100 |
| ), |
| make_uapi_input_event_with_timestamp(uapi::EV_SYN, uapi::SYN_REPORT, 0, 100), |
| ] |
| ); |
| } |
| |
| #[test_case((0.0, 0.0); "origin")] |
| #[test_case((100.7, 200.7); "above midpoint")] |
| #[test_case((100.3, 200.3); "below midpoint")] |
| #[test_case((100.5, 200.5); "midpoint")] |
| #[::fuchsia::test] |
| async fn sends_acceptable_coordinates((x, y): (f32, f32)) { |
| // Set up resources. |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, mut touch_source_stream) = |
| start_touch_input(locked, ¤t_task).await; |
| |
| // Touch add. |
| answer_next_touch_watch_request( |
| &mut touch_source_stream, |
| vec![make_touch_event_with_coords_phase(x, y, EventPhase::Add, 1)], |
| ) |
| .await; |
| |
| // Wait for another `Watch`. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![TouchEvent::default()]) |
| .await; |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| |
| // Check that the reported positions are within the acceptable error. The acceptable |
| // error is chosen to allow either rounding or truncation. |
| const ACCEPTABLE_ERROR: f32 = 1.0; |
| let actual_x = events |
| .iter() |
| .find(|event| { |
| event.type_ == uapi::EV_ABS as u16 && event.code == uapi::ABS_MT_POSITION_X as u16 |
| }) |
| .unwrap_or_else(|| panic!("did not find `ABS_X` event in {:?}", events)) |
| .value; |
| let actual_y = events |
| .iter() |
| .find(|event| { |
| event.type_ == uapi::EV_ABS as u16 && event.code == uapi::ABS_MT_POSITION_Y as u16 |
| }) |
| .unwrap_or_else(|| panic!("did not find `ABS_Y` event in {:?}", events)) |
| .value; |
| assert_near!(x, actual_x as f32, ACCEPTABLE_ERROR); |
| assert_near!(y, actual_y as f32, ACCEPTABLE_ERROR); |
| } |
| |
| // Per the FIDL documentation for `TouchSource::Watch()`: |
| // |
| // > non-sample events should return an empty |TouchResponse| table to the |
| // > server |
| #[test_case( |
| make_touch_event_with_phase(EventPhase::Add, 2) |
| => matches Some(TouchResponse { response_type: Some(_), ..}); |
| "event_with_sample_yields_some_response_type")] |
| #[test_case( |
| TouchEvent::default() => matches Some(TouchResponse { response_type: None, ..}); |
| "event_without_sample_yields_no_response_type")] |
| #[::fuchsia::test] |
| async fn sends_appropriate_reply_to_touch_source_server( |
| event: TouchEvent, |
| ) -> Option<TouchResponse> { |
| // Set up resources. |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, _input_file, mut touch_source_stream) = |
| start_touch_input(locked, ¤t_task).await; |
| |
| // Reply to first `Watch` request. |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![event]).await; |
| |
| // Get response to `event`. |
| let responses = |
| answer_next_touch_watch_request(&mut touch_source_stream, vec![TouchEvent::default()]) |
| .await; |
| |
| // Return the value for `test_case` to match on. |
| responses.get(0).cloned() |
| } |
| |
| #[test_case(fidl_fuchsia_input::Key::Escape, uapi::KEY_POWER; "Esc maps to Power")] |
| #[test_case(fidl_fuchsia_input::Key::A, uapi::KEY_A; "A maps to A")] |
| #[::fuchsia::test] |
| async fn sends_keyboard_events(fkey: fidl_fuchsia_input::Key, lkey: u32) { |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_keyboard_device, keyboard_file, keyboard_listener) = |
| start_keyboard_input(locked, ¤t_task).await; |
| |
| let key_event = fuiinput::KeyEvent { |
| timestamp: Some(0), |
| type_: Some(fuiinput::KeyEventType::Pressed), |
| key: Some(fkey), |
| ..Default::default() |
| }; |
| |
| let _ = keyboard_listener.on_key_event(&key_event).await; |
| std::mem::drop(keyboard_listener); // Close Zircon channel. |
| let events = read_uapi_events(locked, &keyboard_file, ¤t_task); |
| assert_eq!(events.len(), 2); |
| assert_eq!(events[0].code, lkey as u16); |
| } |
| |
| #[::fuchsia::test] |
| async fn skips_unknown_keyboard_events() { |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_keyboard_device, keyboard_file, keyboard_listener) = |
| start_keyboard_input(locked, ¤t_task).await; |
| |
| let key_event = fuiinput::KeyEvent { |
| timestamp: Some(0), |
| type_: Some(fuiinput::KeyEventType::Pressed), |
| key: Some(fidl_fuchsia_input::Key::AcRefresh), |
| ..Default::default() |
| }; |
| |
| let _ = keyboard_listener.on_key_event(&key_event).await; |
| std::mem::drop(keyboard_listener); // Close Zircon channel. |
| let events = read_uapi_events(locked, &keyboard_file, ¤t_task); |
| assert_eq!(events.len(), 0); |
| } |
| |
| #[::fuchsia::test] |
| async fn sends_power_button_events() { |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, buttons_listener) = |
| start_button_input(locked, ¤t_task).await; |
| |
| let power_event = MediaButtonsEvent { |
| volume: Some(0), |
| mic_mute: Some(false), |
| pause: Some(false), |
| camera_disable: Some(false), |
| power: Some(true), |
| function: Some(false), |
| ..Default::default() |
| }; |
| |
| let _ = buttons_listener.on_event(&power_event).await; |
| std::mem::drop(buttons_listener); // Close Zircon channel. |
| |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| assert_eq!(events.len(), 2); |
| assert_eq!(events[0].code, uapi::KEY_POWER as u16); |
| assert_eq!(events[0].value, 1); |
| } |
| |
| #[::fuchsia::test] |
| async fn sends_function_button_events() { |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, buttons_listener) = |
| start_button_input(locked, ¤t_task).await; |
| |
| let function_event = MediaButtonsEvent { |
| volume: Some(0), |
| mic_mute: Some(false), |
| pause: Some(false), |
| camera_disable: Some(false), |
| power: Some(false), |
| function: Some(true), |
| ..Default::default() |
| }; |
| |
| let _ = buttons_listener.on_event(&function_event).await; |
| std::mem::drop(buttons_listener); // Close Zircon channel. |
| |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| assert_eq!(events.len(), 2); |
| assert_eq!(events[0].code, uapi::KEY_VOLUMEDOWN as u16); |
| assert_eq!(events[0].value, 1); |
| } |
| |
| #[::fuchsia::test] |
| async fn sends_overlapping_button_events() { |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, buttons_listener) = |
| start_button_input(locked, ¤t_task).await; |
| |
| let power_event = MediaButtonsEvent { |
| volume: Some(0), |
| mic_mute: Some(false), |
| pause: Some(false), |
| camera_disable: Some(false), |
| power: Some(true), |
| function: Some(false), |
| ..Default::default() |
| }; |
| |
| let function_event = MediaButtonsEvent { |
| volume: Some(0), |
| mic_mute: Some(false), |
| pause: Some(false), |
| camera_disable: Some(false), |
| power: Some(true), |
| function: Some(true), |
| ..Default::default() |
| }; |
| |
| let function_release_event = MediaButtonsEvent { |
| volume: Some(0), |
| mic_mute: Some(false), |
| pause: Some(false), |
| camera_disable: Some(false), |
| power: Some(true), |
| function: Some(false), |
| ..Default::default() |
| }; |
| |
| let power_release_event = MediaButtonsEvent { |
| volume: Some(0), |
| mic_mute: Some(false), |
| pause: Some(false), |
| camera_disable: Some(false), |
| power: Some(false), |
| function: Some(false), |
| ..Default::default() |
| }; |
| |
| let _ = buttons_listener.on_event(&power_event).await; |
| let _ = buttons_listener.on_event(&function_event).await; |
| let _ = buttons_listener.on_event(&function_release_event).await; |
| let _ = buttons_listener.on_event(&power_release_event).await; |
| std::mem::drop(buttons_listener); // Close Zircon channel. |
| |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| assert_eq!(events.len(), 8); |
| assert_eq!(events[0].code, uapi::KEY_POWER as u16); |
| assert_eq!(events[0].value, 1); |
| assert_eq!(events[2].code, uapi::KEY_VOLUMEDOWN as u16); |
| assert_eq!(events[2].value, 1); |
| assert_eq!(events[4].code, uapi::KEY_VOLUMEDOWN as u16); |
| assert_eq!(events[4].value, 0); |
| assert_eq!(events[6].code, uapi::KEY_POWER as u16); |
| assert_eq!(events[6].value, 0); |
| } |
| |
| #[::fuchsia::test] |
| async fn sends_simultaneous_button_events() { |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, buttons_listener) = |
| start_button_input(locked, ¤t_task).await; |
| |
| let power_and_function_event = MediaButtonsEvent { |
| volume: Some(0), |
| mic_mute: Some(false), |
| pause: Some(false), |
| camera_disable: Some(false), |
| power: Some(true), |
| function: Some(true), |
| ..Default::default() |
| }; |
| |
| let _ = buttons_listener.on_event(&power_and_function_event).await; |
| std::mem::drop(buttons_listener); // Close Zircon channel. |
| |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| assert_eq!(events.len(), 4); |
| assert_eq!(events[0].code, uapi::KEY_POWER as u16); |
| assert_eq!(events[0].value, 1); |
| assert_eq!(events[2].code, uapi::KEY_VOLUMEDOWN as u16); |
| assert_eq!(events[2].value, 1); |
| } |
| |
| #[test_case(1; "Scroll up")] |
| #[test_case(-1; "Scroll down")] |
| #[::fuchsia::test] |
| async fn sends_mouse_wheel_events(ticks: i64) { |
| let time = 100; |
| let uapi_event = uapi::input_event { |
| time: timeval_from_time(zx::MonotonicInstant::from_nanos(time)), |
| type_: uapi::EV_REL as u16, |
| code: uapi::REL_WHEEL as u16, |
| value: ticks as i32, |
| }; |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_mouse_device, mouse_file, mut mouse_stream) = |
| start_mouse_input(locked, ¤t_task).await; |
| |
| answer_next_mouse_watch_request( |
| &mut mouse_stream, |
| vec![make_mouse_wheel_event_with_timestamp(ticks, time)], |
| ) |
| .await; |
| |
| // Wait for another `Watch` to ensure mouse_file is done processing the other replies. |
| // Use an empty vec, to ensure no unexpected `uapi::input_event`s are created. |
| answer_next_mouse_watch_request(&mut mouse_stream, vec![]).await; |
| |
| let events = read_uapi_events(locked, &mouse_file, ¤t_task); |
| assert_eq!(events.len(), 2); |
| assert_eq!(events[0], uapi_event); |
| } |
| |
| #[::fuchsia::test] |
| async fn ignore_mouse_non_wheel_events() { |
| let mouse_move_event = fuipointer::MouseEvent { |
| timestamp: Some(0), |
| pointer_sample: Some(fuipointer::MousePointerSample { |
| device_id: Some(0), |
| position_in_viewport: Some([50.0, 50.0]), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }; |
| let mouse_click_event = fuipointer::MouseEvent { |
| timestamp: Some(0), |
| pointer_sample: Some(fuipointer::MousePointerSample { |
| device_id: Some(0), |
| pressed_buttons: Some(vec![1]), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }; |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_mouse_device, mouse_file, mut mouse_stream) = |
| start_mouse_input(locked, ¤t_task).await; |
| |
| // Expect mouse relay to discard MouseEvents without vertical scroll. |
| answer_next_mouse_watch_request(&mut mouse_stream, vec![mouse_move_event]).await; |
| answer_next_mouse_watch_request(&mut mouse_stream, vec![mouse_click_event]).await; |
| |
| // Wait for another `Watch` to ensure mouse_file is done processing the other replies. |
| // Use an empty vec, to ensure no unexpected `uapi::input_event`s are created. |
| answer_next_mouse_watch_request(&mut mouse_stream, vec![]).await; |
| |
| let events = read_uapi_events(locked, &mouse_file, ¤t_task); |
| assert_eq!(events.len(), 0); |
| } |
| |
| #[::fuchsia::test] |
| async fn touch_input_initialized_with_inspect_node() { |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let touch_device = InputDevice::new_touch( |
| 1200, /* screen width */ |
| 720, /* screen height */ |
| &inspector.root(), |
| ); |
| let _file_obj = touch_device.open_test(locked, ¤t_task); |
| |
| assert_data_tree!(inspector, root: { |
| touch_device: { |
| total_fidl_events_received_count: 0u64, |
| total_fidl_events_ignored_count: 0u64, |
| total_fidl_events_unexpected_count: 0u64, |
| total_fidl_events_converted_count: 0u64, |
| total_uapi_events_generated_count: 0u64, |
| last_generated_uapi_event_timestamp_ns: 0i64, |
| touch_file_0: { |
| fidl_events_received_count: 0u64, |
| fidl_events_ignored_count: 0u64, |
| fidl_events_unexpected_count: 0u64, |
| fidl_events_converted_count: 0u64, |
| uapi_events_generated_count: 0u64, |
| uapi_events_read_count: 0u64, |
| last_generated_uapi_event_timestamp_ns: 0i64, |
| last_read_uapi_event_timestamp_ns: 0i64, |
| } |
| } |
| }); |
| } |
| |
| #[::fuchsia::test] |
| async fn touch_relay_updates_touch_inspect_status() { |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, mut touch_source_stream) = |
| start_touch_input_inspect(locked, ¤t_task, &inspector).await; |
| |
| // Send 2 TouchEvents to proxy that should be counted as `received` by InputFile |
| // A TouchEvent::default() has no pointer sample so these events should be discarded. |
| match touch_source_stream.next().await { |
| Some(Ok(TouchSourceRequest::Watch { responder, .. })) => responder |
| .send(&vec![make_empty_touch_event(); 2]) |
| .expect("failure sending Watch reply"), |
| unexpected_request => panic!("unexpected request {:?}", unexpected_request), |
| } |
| |
| // Send 5 TouchEvents with pointer sample to proxy, these should be received and converted |
| // Add/Remove events generate 5 uapi events each. Change events generate 3 uapi events each. |
| match touch_source_stream.next().await { |
| Some(Ok(TouchSourceRequest::Watch { responses, responder })) => { |
| assert_matches!(responses.as_slice(), [_, _]); |
| responder |
| .send(&vec![ |
| make_touch_event_with_coords_phase_timestamp( |
| 0.0, |
| 0.0, |
| EventPhase::Add, |
| 1, |
| 1000, |
| ), |
| make_touch_event_with_coords_phase_timestamp( |
| 0.0, |
| 0.0, |
| EventPhase::Change, |
| 1, |
| 2000, |
| ), |
| make_touch_event_with_coords_phase_timestamp( |
| 0.0, |
| 0.0, |
| EventPhase::Change, |
| 1, |
| 3000, |
| ), |
| make_touch_event_with_coords_phase_timestamp( |
| 0.0, |
| 0.0, |
| EventPhase::Change, |
| 1, |
| 4000, |
| ), |
| make_touch_event_with_coords_phase_timestamp( |
| 0.0, |
| 0.0, |
| EventPhase::Remove, |
| 1, |
| 5000, |
| ), |
| ]) |
| .expect("failure sending Watch reply"); |
| } |
| unexpected_request => panic!("unexpected request {:?}", unexpected_request), |
| } |
| |
| // Wait for next `Watch` call and verify it has five elements in `responses`. |
| match touch_source_stream.next().await { |
| Some(Ok(TouchSourceRequest::Watch { responses, .. })) => { |
| assert_matches!(responses.as_slice(), [_, _, _, _, _]) |
| } |
| unexpected_request => panic!("unexpected request {:?}", unexpected_request), |
| } |
| |
| let _events = read_uapi_events(locked, &input_file, ¤t_task); |
| assert_data_tree!(inspector, root: { |
| touch_device: { |
| total_fidl_events_received_count: 7u64, |
| total_fidl_events_ignored_count: 2u64, |
| total_fidl_events_unexpected_count: 0u64, |
| total_fidl_events_converted_count: 5u64, |
| total_uapi_events_generated_count: 22u64, |
| last_generated_uapi_event_timestamp_ns: 5000i64, |
| touch_file_0: { |
| fidl_events_received_count: 7u64, |
| fidl_events_ignored_count: 2u64, |
| fidl_events_unexpected_count: 0u64, |
| fidl_events_converted_count: 5u64, |
| uapi_events_generated_count: 22u64, |
| uapi_events_read_count: 22u64, |
| last_generated_uapi_event_timestamp_ns: 5000i64, |
| last_read_uapi_event_timestamp_ns: 5000i64, |
| }, |
| } |
| }); |
| } |
| |
| #[::fuchsia::test] |
| async fn new_file_updates_inspect_status() { |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| |
| let input_device = InputDevice::new_touch(700, 700, inspector.root()); |
| let input_file_0 = |
| input_device.open_test(locked, ¤t_task).expect("Failed to create input file"); |
| |
| let (touch_source_client_end, mut touch_source_stream) = |
| fidl::endpoints::create_request_stream::<TouchSourceMarker>(); |
| let (mouse_source_client_end, _mouse_source_stream) = |
| fidl::endpoints::create_request_stream::<fuipointer::MouseSourceMarker>(); |
| let (keyboard_proxy, mut keyboard_stream) = |
| fidl::endpoints::create_sync_proxy_and_stream::<fuiinput::KeyboardMarker>(); |
| let view_ref_pair = |
| fuchsia_scenic::ViewRefPair::new().expect("Failed to create ViewRefPair"); |
| let (device_registry_proxy, mut device_listener_stream) = |
| fidl::endpoints::create_sync_proxy_and_stream::<fuipolicy::DeviceListenerRegistryMarker>( |
| ); |
| |
| let (relay, relay_handle) = input_event_relay::new_input_relay(); |
| relay.start_relays( |
| ¤t_task.kernel(), |
| EventProxyMode::None, |
| touch_source_client_end, |
| keyboard_proxy, |
| mouse_source_client_end, |
| view_ref_pair.view_ref, |
| device_registry_proxy, |
| input_device.open_files.clone(), |
| Default::default(), |
| Default::default(), |
| Some(input_device.inspect_status.clone()), |
| None, |
| None, |
| ); |
| |
| let _ = init_keyboard_listener(&mut keyboard_stream).await; |
| let _ = init_button_listeners(&mut device_listener_stream).await; |
| |
| relay_handle.add_touch_device( |
| 0, |
| input_device.open_files.clone(), |
| Some(input_device.inspect_status.clone()), |
| ); |
| |
| // Send 2 TouchEvents to proxy that should be counted as `received` by InputFile |
| // A TouchEvent::default() has no pointer sample so these events should be discarded. |
| match touch_source_stream.next().await { |
| Some(Ok(TouchSourceRequest::Watch { responder, .. })) => responder |
| .send(&vec![make_empty_touch_event(); 2]) |
| .expect("failure sending Watch reply"), |
| unexpected_request => panic!("unexpected request {:?}", unexpected_request), |
| } |
| |
| // Wait for next `Watch` call and verify it has two elements in `responses`. |
| match touch_source_stream.next().await { |
| Some(Ok(TouchSourceRequest::Watch { responses, responder })) => { |
| assert_matches!(responses.as_slice(), [_, _]); |
| responder.send(&vec![]).expect("failure sending Watch reply"); |
| } |
| unexpected_request => panic!("unexpected request {:?}", unexpected_request), |
| } |
| |
| // Verify file node & properties remain in inspect tree when file is closed |
| input_device.open_files.lock().clear(); |
| drop(input_file_0); |
| |
| // Open new file which should receive input_device's subsequent events |
| let input_file_1 = |
| input_device.open_test(locked, ¤t_task).expect("Failed to create input file"); |
| |
| // Send 5 TouchEvents with pointer sample to proxy, these should be received and converted |
| // Add/Remove events generate 5 uapi events each. Change events generate 3 uapi events each. |
| match touch_source_stream.next().await { |
| Some(Ok(TouchSourceRequest::Watch { responder, .. })) => { |
| responder |
| .send(&vec![ |
| make_touch_event_with_coords_phase_timestamp( |
| 0.0, |
| 0.0, |
| EventPhase::Add, |
| 1, |
| 1000, |
| ), |
| make_touch_event_with_coords_phase_timestamp( |
| 0.0, |
| 0.0, |
| EventPhase::Change, |
| 1, |
| 2000, |
| ), |
| make_touch_event_with_coords_phase_timestamp( |
| 0.0, |
| 0.0, |
| EventPhase::Change, |
| 1, |
| 3000, |
| ), |
| make_touch_event_with_coords_phase_timestamp( |
| 0.0, |
| 0.0, |
| EventPhase::Change, |
| 1, |
| 4000, |
| ), |
| make_touch_event_with_coords_phase_timestamp( |
| 0.0, |
| 0.0, |
| EventPhase::Remove, |
| 1, |
| 5000, |
| ), |
| ]) |
| .expect("failure sending Watch reply"); |
| } |
| unexpected_request => panic!("unexpected request {:?}", unexpected_request), |
| } |
| |
| // Wait for next `Watch` call and verify it has five elements in `responses`. |
| match touch_source_stream.next().await { |
| Some(Ok(TouchSourceRequest::Watch { responses, .. })) => { |
| assert_matches!(responses.as_slice(), [_, _, _, _, _]) |
| } |
| unexpected_request => panic!("unexpected request {:?}", unexpected_request), |
| } |
| |
| let _events = read_uapi_events(locked, &input_file_1, ¤t_task); |
| |
| // Verify file node & properties remain in inspect tree when file is closed |
| input_device.open_files.lock().clear(); |
| drop(input_file_1); |
| |
| assert_data_tree!(inspector, root: { |
| touch_device: { |
| total_fidl_events_received_count: 7u64, |
| total_fidl_events_ignored_count: 2u64, |
| total_fidl_events_unexpected_count: 0u64, |
| total_fidl_events_converted_count: 5u64, |
| total_uapi_events_generated_count: 22u64, |
| last_generated_uapi_event_timestamp_ns: 5000i64, |
| touch_file_0: { |
| fidl_events_received_count: 2u64, |
| fidl_events_ignored_count: 2u64, |
| fidl_events_unexpected_count: 0u64, |
| fidl_events_converted_count: 0u64, |
| uapi_events_generated_count: 0u64, |
| uapi_events_read_count: 0u64, |
| last_generated_uapi_event_timestamp_ns: 0i64, |
| last_read_uapi_event_timestamp_ns: 0i64, |
| }, |
| touch_file_1: { |
| fidl_events_received_count: 5u64, |
| fidl_events_ignored_count: 0u64, |
| fidl_events_unexpected_count: 0u64, |
| fidl_events_converted_count: 5u64, |
| uapi_events_generated_count: 22u64, |
| uapi_events_read_count: 22u64, |
| last_generated_uapi_event_timestamp_ns: 5000i64, |
| last_read_uapi_event_timestamp_ns: 5000i64, |
| }, |
| } |
| }); |
| } |
| |
| #[::fuchsia::test] |
| async fn keyboard_input_initialized_with_inspect_node() { |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let keyboard_device = InputDevice::new_keyboard(&inspector.root()); |
| let _file_obj = keyboard_device.open_test(locked, ¤t_task); |
| |
| assert_data_tree!(inspector, root: { |
| keyboard_device: { |
| total_fidl_events_received_count: 0u64, |
| total_fidl_events_ignored_count: 0u64, |
| total_fidl_events_unexpected_count: 0u64, |
| total_fidl_events_converted_count: 0u64, |
| total_uapi_events_generated_count: 0u64, |
| last_generated_uapi_event_timestamp_ns: 0i64, |
| keyboard_file_0: { |
| fidl_events_received_count: 0u64, |
| fidl_events_ignored_count: 0u64, |
| fidl_events_unexpected_count: 0u64, |
| fidl_events_converted_count: 0u64, |
| uapi_events_generated_count: 0u64, |
| uapi_events_read_count: 0u64, |
| last_generated_uapi_event_timestamp_ns: 0i64, |
| last_read_uapi_event_timestamp_ns: 0i64, |
| } |
| } |
| }); |
| } |
| |
| #[::fuchsia::test] |
| async fn button_relay_updates_keyboard_inspect_status() { |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, buttons_listener) = |
| start_button_input_inspect(locked, ¤t_task, &inspector).await; |
| |
| // Each of these events should count toward received and converted. |
| // They also generate 2 uapi events each. |
| let power_event = MediaButtonsEvent { |
| volume: Some(0), |
| mic_mute: Some(false), |
| pause: Some(false), |
| camera_disable: Some(false), |
| power: Some(true), |
| function: Some(false), |
| ..Default::default() |
| }; |
| |
| let power_release_event = MediaButtonsEvent { |
| volume: Some(0), |
| mic_mute: Some(false), |
| pause: Some(false), |
| camera_disable: Some(false), |
| power: Some(false), |
| function: Some(false), |
| ..Default::default() |
| }; |
| |
| let _ = buttons_listener.on_event(&power_event).await; |
| let _ = buttons_listener.on_event(&power_release_event).await; |
| |
| let events = read_uapi_events(locked, &input_file, ¤t_task); |
| assert_eq!(events.len(), 4); |
| assert_eq!(events[0].code, uapi::KEY_POWER as u16); |
| assert_eq!(events[0].value, 1); |
| assert_eq!(events[2].code, uapi::KEY_POWER as u16); |
| assert_eq!(events[2].value, 0); |
| |
| let _events = read_uapi_events(locked, &input_file, ¤t_task); |
| |
| assert_data_tree!(inspector, root: { |
| keyboard_device: { |
| total_fidl_events_received_count: 2u64, |
| total_fidl_events_ignored_count: 0u64, |
| total_fidl_events_unexpected_count: 0u64, |
| total_fidl_events_converted_count: 2u64, |
| total_uapi_events_generated_count: 4u64, |
| // Button events perform a realtime clockread, so any value will do. |
| last_generated_uapi_event_timestamp_ns: AnyProperty, |
| keyboard_file_0: { |
| fidl_events_received_count: 2u64, |
| fidl_events_ignored_count: 0u64, |
| fidl_events_unexpected_count: 0u64, |
| fidl_events_converted_count: 2u64, |
| uapi_events_generated_count: 4u64, |
| uapi_events_read_count: 4u64, |
| // Button events perform a realtime clockread, so any value will do. |
| last_generated_uapi_event_timestamp_ns: AnyProperty, |
| last_read_uapi_event_timestamp_ns: AnyProperty, |
| }, |
| } |
| }); |
| } |
| |
| #[::fuchsia::test] |
| async fn mouse_input_initialized_with_inspect_node() { |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let mouse_device = InputDevice::new_mouse(&inspector.root()); |
| let _file_obj = mouse_device.open_test(locked, ¤t_task); |
| |
| assert_data_tree!(inspector, root: { |
| mouse_device: { |
| total_fidl_events_received_count: 0u64, |
| total_fidl_events_ignored_count: 0u64, |
| total_fidl_events_unexpected_count: 0u64, |
| total_fidl_events_converted_count: 0u64, |
| total_uapi_events_generated_count: 0u64, |
| last_generated_uapi_event_timestamp_ns: 0i64, |
| mouse_file_0: { |
| fidl_events_received_count: 0u64, |
| fidl_events_ignored_count: 0u64, |
| fidl_events_unexpected_count: 0u64, |
| fidl_events_converted_count: 0u64, |
| uapi_events_generated_count: 0u64, |
| uapi_events_read_count: 0u64, |
| last_generated_uapi_event_timestamp_ns: 0i64, |
| last_read_uapi_event_timestamp_ns: 0i64, |
| } |
| } |
| }); |
| } |
| |
| #[::fuchsia::test] |
| async fn mouse_relay_updates_mouse_inspect_status() { |
| let inspector = fuchsia_inspect::Inspector::default(); |
| let (_kernel, current_task, locked) = create_kernel_task_and_unlocked(); |
| let (_input_device, input_file, mut mouse_source_stream) = |
| start_mouse_input_inspect(locked, ¤t_task, &inspector).await; |
| |
| let mouse_move_event = fuipointer::MouseEvent { |
| timestamp: Some(0), |
| pointer_sample: Some(fuipointer::MousePointerSample { |
| device_id: Some(0), |
| position_in_viewport: Some([50.0, 50.0]), |
| scroll_v: Some(0), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }; |
| let mouse_click_event = fuipointer::MouseEvent { |
| timestamp: Some(0), |
| pointer_sample: Some(fuipointer::MousePointerSample { |
| device_id: Some(0), |
| scroll_v: Some(0), |
| pressed_buttons: Some(vec![1]), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }; |
| |
| // Send 2 non-wheel MouseEvents to proxy that should be counted as `received` by InputFile |
| // These events have no scroll_v delta in the pointer sample so they should be ignored. |
| answer_next_mouse_watch_request( |
| &mut mouse_source_stream, |
| vec![mouse_move_event, mouse_click_event], |
| ) |
| .await; |
| |
| // Send 5 MouseEvents with non-zero scroll_v delta to proxy, these should be received and |
| // converted to 1 uapi event each, with an extra sync event to signify end of the batch. |
| answer_next_mouse_watch_request( |
| &mut mouse_source_stream, |
| vec![make_mouse_wheel_event(1); 5], |
| ) |
| .await; |
| |
| // Send a final mouse wheel event and ensure the inspect tree reflects it's timestamp under |
| // last_generated_uapi_event_timestamp_ns and last_read_uapi_event_timestamp_ns. |
| answer_next_mouse_watch_request( |
| &mut mouse_source_stream, |
| vec![make_mouse_wheel_event_with_timestamp(-1, 5000)], |
| ) |
| .await; |
| |
| // Wait for another `Watch` to ensure mouse_file is done processing the other replies. |
| // Use an empty vec, to ensure no unexpected `uapi::input_event`s are created. |
| answer_next_mouse_watch_request(&mut mouse_source_stream, vec![]).await; |
| |
| let _events = read_uapi_events(locked, &input_file, ¤t_task); |
| assert_data_tree!(inspector, root: { |
| mouse_device: { |
| total_fidl_events_received_count: 8u64, |
| total_fidl_events_ignored_count: 2u64, |
| total_fidl_events_unexpected_count: 0u64, |
| total_fidl_events_converted_count: 6u64, |
| total_uapi_events_generated_count: 8u64, |
| last_generated_uapi_event_timestamp_ns: 5000i64, |
| mouse_file_0: { |
| fidl_events_received_count: 8u64, |
| fidl_events_ignored_count: 2u64, |
| fidl_events_unexpected_count: 0u64, |
| fidl_events_converted_count: 6u64, |
| uapi_events_generated_count: 8u64, |
| uapi_events_read_count: 8u64, |
| last_generated_uapi_event_timestamp_ns: 5000i64, |
| last_read_uapi_event_timestamp_ns: 5000i64, |
| }, |
| } |
| }); |
| } |
| } |