Skip to main content

starnix_modules_input_report_event_conversion/
touch.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use fidl_fuchsia_input_report as fir;
6use sorted_vec_map::{SortedVecMap, SortedVecSet};
7use starnix_types::time::timeval_from_time;
8use starnix_uapi::uapi;
9
10const DEFAULT_MAX_TOUCH_CONTACTS: usize = 10;
11const LIFTED_TRACKING_ID: i32 = -1;
12
13/// Maps a Fuchsia `TouchButton` to a Linux keycode.
14pub fn touch_button_to_linux_key(button: fir::TouchButton) -> Option<u32> {
15    match button {
16        fir::TouchButton::Palm => Some(uapi::KEY_SLEEP),
17        fir::TouchButton::SwipeUp => Some(uapi::KEY_UP),
18        fir::TouchButton::SwipeLeft => Some(uapi::KEY_LEFT),
19        fir::TouchButton::SwipeRight => Some(uapi::KEY_RIGHT),
20        fir::TouchButton::SwipeDown => Some(uapi::KEY_DOWN),
21        _ => None,
22    }
23}
24
25#[derive(Debug, Clone, PartialEq, Default)]
26struct ContactState {
27    pos_x: i32,
28    pos_y: i32,
29    pressure: Option<i32>,
30    touch_major: Option<i32>,
31    touch_minor: Option<i32>,
32}
33
34/// Converts `TouchInputReport` events into Linux Multi-Touch Protocol B `uapi::input_event`s.
35#[derive(Debug, PartialEq)]
36pub struct TouchReportConverter {
37    pointer_id_to_slot_id: SortedVecMap<u32, usize>,
38    slot_to_state: SortedVecMap<usize, ContactState>,
39    pressed_touch_buttons: SortedVecSet<u16>,
40    max_slots: usize,
41}
42
43impl Default for TouchReportConverter {
44    fn default() -> Self {
45        Self {
46            pointer_id_to_slot_id: SortedVecMap::new(),
47            slot_to_state: SortedVecMap::new(),
48            pressed_touch_buttons: SortedVecSet::new(),
49            max_slots: DEFAULT_MAX_TOUCH_CONTACTS,
50        }
51    }
52}
53
54impl TouchReportConverter {
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    pub fn with_max_slots(max_slots: usize) -> Self {
60        Self {
61            pointer_id_to_slot_id: SortedVecMap::new(),
62            slot_to_state: SortedVecMap::new(),
63            pressed_touch_buttons: SortedVecSet::new(),
64            max_slots,
65        }
66    }
67
68    pub fn from_touch_input_descriptor(input: &fir::TouchInputDescriptor) -> Self {
69        let max_slots = input
70            .max_contacts
71            .map(|m| m as usize)
72            .or_else(|| input.contacts.as_ref().map(|c| c.len()))
73            .unwrap_or(DEFAULT_MAX_TOUCH_CONTACTS)
74            .max(1);
75        Self::with_max_slots(max_slots)
76    }
77
78    pub fn from_touch_descriptor(descriptor: &fir::TouchDescriptor) -> Self {
79        descriptor.input.as_ref().map(Self::from_touch_input_descriptor).unwrap_or_default()
80    }
81
82    pub fn max_slots(&self) -> usize {
83        self.max_slots
84    }
85
86    fn available_slot_id(&self) -> Option<usize> {
87        let mut used_slots = bit_vec::BitVec::<u32>::from_elem(self.max_slots, false);
88        for &slot in self.pointer_id_to_slot_id.values() {
89            if slot < self.max_slots {
90                used_slots.set(slot, true);
91            }
92        }
93        used_slots.iter().position(|used| !used)
94    }
95
96    pub fn handle(
97        &mut self,
98        report: &fir::TouchInputReport,
99        event_time: zx::MonotonicInstant,
100    ) -> Vec<uapi::input_event> {
101        let mut events = Vec::new();
102        let time = timeval_from_time(event_time);
103
104        let current_touch_buttons = report.pressed_buttons.as_ref().map(|buttons| {
105            let mut set = SortedVecSet::new();
106            for &button in buttons.iter() {
107                if let Some(key) = touch_button_to_linux_key(button) {
108                    set.insert(key as u16);
109                }
110            }
111            set
112        });
113
114        if let Some(ref contacts) = report.contacts {
115            let was_empty = self.pointer_id_to_slot_id.is_empty();
116            let is_button_report_with_empty_contacts = contacts.is_empty()
117                && current_touch_buttons
118                    .as_ref()
119                    .is_some_and(|current| *current != self.pressed_touch_buttons);
120
121            let mut current_contact_ids = SortedVecSet::new();
122
123            for contact in contacts.iter() {
124                let Some(contact_id) = contact.contact_id else {
125                    continue;
126                };
127
128                // Track contact_id immediately in current_contact_ids.
129                // Linux MT Protocol B requires each slot/contact to appear at most once per sync frame.
130                // If contact_id is duplicate in this report, drop it.
131                if !current_contact_ids.insert(contact_id) {
132                    continue;
133                }
134
135                // If coordinates are missing, skip emitting slot updates for this contact,
136                // but keep it in current_contact_ids so it is not falsely considered lifted.
137                let (Some(raw_pos_x), Some(raw_pos_y)) = (contact.position_x, contact.position_y)
138                else {
139                    continue;
140                };
141                let pos_x = crate::clamp_i64_to_i32(raw_pos_x);
142                let pos_y = crate::clamp_i64_to_i32(raw_pos_y);
143
144                let is_new = !self.pointer_id_to_slot_id.contains_key(&contact_id);
145                let slot = if let Some(&existing_slot) = self.pointer_id_to_slot_id.get(&contact_id)
146                {
147                    existing_slot
148                } else if let Some(new_slot) = self.available_slot_id() {
149                    self.pointer_id_to_slot_id.insert(contact_id, new_slot);
150                    new_slot
151                } else {
152                    // All slots exhausted, skip this contact.
153                    continue;
154                };
155
156                let pressure = contact.pressure.map(crate::clamp_i64_to_i32);
157                let (touch_major, touch_minor) =
158                    match (contact.contact_width, contact.contact_height) {
159                        (Some(w), Some(h)) => {
160                            let major = std::cmp::max(w, h);
161                            let minor = std::cmp::min(w, h);
162                            (
163                                Some(crate::clamp_i64_to_i32(major)),
164                                Some(crate::clamp_i64_to_i32(minor)),
165                            )
166                        }
167                        (Some(w), None) => (Some(crate::clamp_i64_to_i32(w)), None),
168                        (None, Some(h)) => (Some(crate::clamp_i64_to_i32(h)), None),
169                        (None, None) => (None, None),
170                    };
171
172                if is_new {
173                    let new_state =
174                        ContactState { pos_x, pos_y, pressure, touch_major, touch_minor };
175                    events.push(uapi::input_event {
176                        time,
177                        type_: uapi::EV_ABS as u16,
178                        code: uapi::ABS_MT_SLOT as u16,
179                        value: slot as i32,
180                    });
181                    // Mask contact_id with 0x7fff_ffff so it never evaluates to negative (e.g. -1 for lift).
182                    events.push(uapi::input_event {
183                        time,
184                        type_: uapi::EV_ABS as u16,
185                        code: uapi::ABS_MT_TRACKING_ID as u16,
186                        value: (contact_id & 0x7fff_ffff) as i32,
187                    });
188                    events.push(uapi::input_event {
189                        time,
190                        type_: uapi::EV_ABS as u16,
191                        code: uapi::ABS_MT_POSITION_X as u16,
192                        value: pos_x,
193                    });
194                    events.push(uapi::input_event {
195                        time,
196                        type_: uapi::EV_ABS as u16,
197                        code: uapi::ABS_MT_POSITION_Y as u16,
198                        value: pos_y,
199                    });
200                    if let Some(p) = pressure {
201                        events.push(uapi::input_event {
202                            time,
203                            type_: uapi::EV_ABS as u16,
204                            code: uapi::ABS_MT_PRESSURE as u16,
205                            value: p,
206                        });
207                    }
208                    if let Some(major) = touch_major {
209                        events.push(uapi::input_event {
210                            time,
211                            type_: uapi::EV_ABS as u16,
212                            code: uapi::ABS_MT_TOUCH_MAJOR as u16,
213                            value: major,
214                        });
215                    }
216                    if let Some(minor) = touch_minor {
217                        events.push(uapi::input_event {
218                            time,
219                            type_: uapi::EV_ABS as u16,
220                            code: uapi::ABS_MT_TOUCH_MINOR as u16,
221                            value: minor,
222                        });
223                    }
224                    self.slot_to_state.insert(slot, new_state);
225                } else {
226                    let old_state = self.slot_to_state.get(&slot);
227                    let changed_x = old_state.map_or(true, |s| s.pos_x != pos_x);
228                    let changed_y = old_state.map_or(true, |s| s.pos_y != pos_y);
229                    let changed_pressure =
230                        pressure.is_some() && old_state.map_or(true, |s| s.pressure != pressure);
231                    let changed_major = touch_major.is_some()
232                        && old_state.map_or(true, |s| s.touch_major != touch_major);
233                    let changed_minor = touch_minor.is_some()
234                        && old_state.map_or(true, |s| s.touch_minor != touch_minor);
235
236                    if changed_x || changed_y || changed_pressure || changed_major || changed_minor
237                    {
238                        let new_state = ContactState {
239                            pos_x,
240                            pos_y,
241                            pressure: pressure.or_else(|| old_state.and_then(|s| s.pressure)),
242                            touch_major: touch_major
243                                .or_else(|| old_state.and_then(|s| s.touch_major)),
244                            touch_minor: touch_minor
245                                .or_else(|| old_state.and_then(|s| s.touch_minor)),
246                        };
247                        events.push(uapi::input_event {
248                            time,
249                            type_: uapi::EV_ABS as u16,
250                            code: uapi::ABS_MT_SLOT as u16,
251                            value: slot as i32,
252                        });
253                        if changed_x {
254                            events.push(uapi::input_event {
255                                time,
256                                type_: uapi::EV_ABS as u16,
257                                code: uapi::ABS_MT_POSITION_X as u16,
258                                value: pos_x,
259                            });
260                        }
261                        if changed_y {
262                            events.push(uapi::input_event {
263                                time,
264                                type_: uapi::EV_ABS as u16,
265                                code: uapi::ABS_MT_POSITION_Y as u16,
266                                value: pos_y,
267                            });
268                        }
269                        if changed_pressure {
270                            if let Some(p) = pressure {
271                                events.push(uapi::input_event {
272                                    time,
273                                    type_: uapi::EV_ABS as u16,
274                                    code: uapi::ABS_MT_PRESSURE as u16,
275                                    value: p,
276                                });
277                            }
278                        }
279                        if changed_major {
280                            if let Some(major) = touch_major {
281                                events.push(uapi::input_event {
282                                    time,
283                                    type_: uapi::EV_ABS as u16,
284                                    code: uapi::ABS_MT_TOUCH_MAJOR as u16,
285                                    value: major,
286                                });
287                            }
288                        }
289                        if changed_minor {
290                            if let Some(minor) = touch_minor {
291                                events.push(uapi::input_event {
292                                    time,
293                                    type_: uapi::EV_ABS as u16,
294                                    code: uapi::ABS_MT_TOUCH_MINOR as u16,
295                                    value: minor,
296                                });
297                            }
298                        }
299                        self.slot_to_state.insert(slot, new_state);
300                    }
301                }
302            }
303
304            // Detect and emit lifted contacts unless this is a button report with empty contacts.
305            if !is_button_report_with_empty_contacts {
306                let removed_contacts: Vec<(u32, usize)> = self
307                    .pointer_id_to_slot_id
308                    .iter()
309                    .filter(|(id, _)| !current_contact_ids.contains(id))
310                    .map(|(&id, &slot)| (id, slot))
311                    .collect();
312
313                for (id, slot) in removed_contacts {
314                    events.push(uapi::input_event {
315                        time,
316                        type_: uapi::EV_ABS as u16,
317                        code: uapi::ABS_MT_SLOT as u16,
318                        value: slot as i32,
319                    });
320                    events.push(uapi::input_event {
321                        time,
322                        type_: uapi::EV_ABS as u16,
323                        code: uapi::ABS_MT_TRACKING_ID as u16,
324                        value: LIFTED_TRACKING_ID,
325                    });
326                    self.pointer_id_to_slot_id.remove(&id);
327                    self.slot_to_state.remove(&slot);
328                }
329
330                // Emit BTN_TOUCH transition if needed.
331                let is_empty = self.pointer_id_to_slot_id.is_empty();
332                if was_empty && !is_empty {
333                    events.push(uapi::input_event {
334                        time,
335                        type_: uapi::EV_KEY as u16,
336                        code: uapi::BTN_TOUCH as u16,
337                        value: 1,
338                    });
339                } else if !was_empty && is_empty {
340                    events.push(uapi::input_event {
341                        time,
342                        type_: uapi::EV_KEY as u16,
343                        code: uapi::BTN_TOUCH as u16,
344                        value: 0,
345                    });
346                }
347            }
348        }
349
350        // Handle touch buttons (e.g. Palm, Swipes).
351        if let Some(current_buttons) = current_touch_buttons {
352            crate::emit_key_transitions(
353                &mut self.pressed_touch_buttons,
354                current_buttons,
355                time,
356                &mut events,
357            );
358        }
359
360        if !events.is_empty() {
361            events.push(uapi::input_event {
362                time,
363                type_: uapi::EV_SYN as u16,
364                code: uapi::SYN_REPORT as u16,
365                value: 0,
366            });
367        }
368
369        events
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[::fuchsia::test]
378    fn test_touch_report_single_contact_lifecycle() {
379        let mut converter = TouchReportConverter::new();
380        let time = zx::MonotonicInstant::from_nanos(100_000_000);
381
382        // 1. Initial contact down at (100, 200)
383        let report_down = fir::TouchInputReport {
384            contacts: Some(vec![fir::ContactInputReport {
385                contact_id: Some(1),
386                position_x: Some(100),
387                position_y: Some(200),
388                pressure: Some(50),
389                ..Default::default()
390            }]),
391            ..Default::default()
392        };
393
394        let events = converter.handle(&report_down, time);
395        assert_eq!(
396            events.iter().map(|e| (e.type_, e.code, e.value)).collect::<Vec<_>>(),
397            vec![
398                (uapi::EV_ABS as u16, uapi::ABS_MT_SLOT as u16, 0),
399                (uapi::EV_ABS as u16, uapi::ABS_MT_TRACKING_ID as u16, 1),
400                (uapi::EV_ABS as u16, uapi::ABS_MT_POSITION_X as u16, 100),
401                (uapi::EV_ABS as u16, uapi::ABS_MT_POSITION_Y as u16, 200),
402                (uapi::EV_ABS as u16, uapi::ABS_MT_PRESSURE as u16, 50),
403                (uapi::EV_KEY as u16, uapi::BTN_TOUCH as u16, 1),
404                (uapi::EV_SYN as u16, uapi::SYN_REPORT as u16, 0),
405            ]
406        );
407
408        // 2. Contact move to (150, 250)
409        let report_move = fir::TouchInputReport {
410            contacts: Some(vec![fir::ContactInputReport {
411                contact_id: Some(1),
412                position_x: Some(150),
413                position_y: Some(250),
414                pressure: Some(60),
415                ..Default::default()
416            }]),
417            ..Default::default()
418        };
419
420        let events = converter.handle(&report_move, time);
421        assert_eq!(
422            events.iter().map(|e| (e.type_, e.code, e.value)).collect::<Vec<_>>(),
423            vec![
424                (uapi::EV_ABS as u16, uapi::ABS_MT_SLOT as u16, 0),
425                (uapi::EV_ABS as u16, uapi::ABS_MT_POSITION_X as u16, 150),
426                (uapi::EV_ABS as u16, uapi::ABS_MT_POSITION_Y as u16, 250),
427                (uapi::EV_ABS as u16, uapi::ABS_MT_PRESSURE as u16, 60),
428                (uapi::EV_SYN as u16, uapi::SYN_REPORT as u16, 0),
429            ]
430        );
431
432        // 3. Contact lift (empty contacts)
433        let report_up = fir::TouchInputReport { contacts: Some(vec![]), ..Default::default() };
434
435        let events = converter.handle(&report_up, time);
436        assert_eq!(
437            events.iter().map(|e| (e.type_, e.code, e.value)).collect::<Vec<_>>(),
438            vec![
439                (uapi::EV_ABS as u16, uapi::ABS_MT_SLOT as u16, 0),
440                (uapi::EV_ABS as u16, uapi::ABS_MT_TRACKING_ID as u16, -1),
441                (uapi::EV_KEY as u16, uapi::BTN_TOUCH as u16, 0),
442                (uapi::EV_SYN as u16, uapi::SYN_REPORT as u16, 0),
443            ]
444        );
445    }
446
447    #[::fuchsia::test]
448    fn test_touch_report_multi_contact() {
449        let mut converter = TouchReportConverter::new();
450        let time = zx::MonotonicInstant::from_nanos(200_000_000);
451
452        // Contact 1 down
453        let report1 = fir::TouchInputReport {
454            contacts: Some(vec![fir::ContactInputReport {
455                contact_id: Some(10),
456                position_x: Some(50),
457                position_y: Some(60),
458                ..Default::default()
459            }]),
460            ..Default::default()
461        };
462        let events1 = converter.handle(&report1, time);
463        assert_eq!(events1[0].code, uapi::ABS_MT_SLOT as u16);
464        assert_eq!(events1[0].value, 0);
465
466        // Contact 2 down alongside Contact 1
467        let report2 = fir::TouchInputReport {
468            contacts: Some(vec![
469                fir::ContactInputReport {
470                    contact_id: Some(10),
471                    position_x: Some(52),
472                    position_y: Some(62),
473                    ..Default::default()
474                },
475                fir::ContactInputReport {
476                    contact_id: Some(20),
477                    position_x: Some(300),
478                    position_y: Some(400),
479                    ..Default::default()
480                },
481            ]),
482            ..Default::default()
483        };
484        let events2 = converter.handle(&report2, time);
485        // Slot 0 for contact 10, Slot 1 for contact 20
486        assert!(events2.iter().any(|e| e.code == uapi::ABS_MT_SLOT as u16 && e.value == 0));
487        assert!(events2.iter().any(|e| e.code == uapi::ABS_MT_SLOT as u16 && e.value == 1));
488        assert!(events2.iter().any(|e| e.code == uapi::ABS_MT_TRACKING_ID as u16 && e.value == 20));
489
490        // Contact 10 lifted, Contact 20 stays
491        let report3 = fir::TouchInputReport {
492            contacts: Some(vec![fir::ContactInputReport {
493                contact_id: Some(20),
494                position_x: Some(305),
495                position_y: Some(405),
496                ..Default::default()
497            }]),
498            ..Default::default()
499        };
500        let events3 = converter.handle(&report3, time);
501        // Slot 0 lifted
502        assert!(events3.iter().any(|e| e.code == uapi::ABS_MT_SLOT as u16 && e.value == 0));
503        assert!(events3.iter().any(|e| e.code == uapi::ABS_MT_TRACKING_ID as u16 && e.value == -1));
504        // BTN_TOUCH remains active (not 0) because contact 20 is still down
505        assert!(!events3.iter().any(|e| e.code == uapi::BTN_TOUCH as u16 && e.value == 0));
506
507        // Contact 20 lifted
508        let report4 = fir::TouchInputReport { contacts: Some(vec![]), ..Default::default() };
509        let events4 = converter.handle(&report4, time);
510        assert!(events4.iter().any(|e| e.code == uapi::ABS_MT_SLOT as u16 && e.value == 1));
511        assert!(events4.iter().any(|e| e.code == uapi::ABS_MT_TRACKING_ID as u16 && e.value == -1));
512        assert!(events4.iter().any(|e| e.code == uapi::BTN_TOUCH as u16 && e.value == 0));
513    }
514
515    #[::fuchsia::test]
516    fn test_touch_report_buttons() {
517        let mut converter = TouchReportConverter::new();
518        let time = zx::MonotonicInstant::from_nanos(300_000_000);
519
520        // Press Palm
521        let report_press = fir::TouchInputReport {
522            pressed_buttons: Some(vec![fir::TouchButton::Palm]),
523            ..Default::default()
524        };
525        let events = converter.handle(&report_press, time);
526        assert_eq!(
527            events.iter().map(|e| (e.type_, e.code, e.value)).collect::<Vec<_>>(),
528            vec![
529                (uapi::EV_KEY as u16, uapi::KEY_SLEEP as u16, 1),
530                (uapi::EV_SYN as u16, uapi::SYN_REPORT as u16, 0),
531            ]
532        );
533
534        // Release Palm
535        let report_release =
536            fir::TouchInputReport { pressed_buttons: Some(vec![]), ..Default::default() };
537        let events = converter.handle(&report_release, time);
538        assert_eq!(
539            events.iter().map(|e| (e.type_, e.code, e.value)).collect::<Vec<_>>(),
540            vec![
541                (uapi::EV_KEY as u16, uapi::KEY_SLEEP as u16, 0),
542                (uapi::EV_SYN as u16, uapi::SYN_REPORT as u16, 0),
543            ]
544        );
545    }
546
547    #[::fuchsia::test]
548    fn test_touch_report_contact_size_normalization() {
549        let mut converter = TouchReportConverter::new();
550        let time = zx::MonotonicInstant::from_nanos(100_000_000);
551
552        // Case 1: height > width (should normalize major=height, minor=width)
553        let report1 = fir::TouchInputReport {
554            contacts: Some(vec![fir::ContactInputReport {
555                contact_id: Some(1),
556                position_x: Some(10),
557                position_y: Some(20),
558                contact_width: Some(30),
559                contact_height: Some(80),
560                ..Default::default()
561            }]),
562            ..Default::default()
563        };
564        let events1 = converter.handle(&report1, time);
565        assert!(events1.iter().any(|e| e.code == uapi::ABS_MT_TOUCH_MAJOR as u16 && e.value == 80));
566        assert!(events1.iter().any(|e| e.code == uapi::ABS_MT_TOUCH_MINOR as u16 && e.value == 30));
567
568        // Case 2: width > height (should normalize major=width, minor=height)
569        let report2 = fir::TouchInputReport {
570            contacts: Some(vec![fir::ContactInputReport {
571                contact_id: Some(1),
572                position_x: Some(15),
573                position_y: Some(25),
574                contact_width: Some(100),
575                contact_height: Some(50),
576                ..Default::default()
577            }]),
578            ..Default::default()
579        };
580        let events2 = converter.handle(&report2, time);
581        assert!(
582            events2.iter().any(|e| e.code == uapi::ABS_MT_TOUCH_MAJOR as u16 && e.value == 100)
583        );
584        assert!(events2.iter().any(|e| e.code == uapi::ABS_MT_TOUCH_MINOR as u16 && e.value == 50));
585    }
586
587    #[::fuchsia::test]
588    fn test_touch_report_tracking_id_sign_guard() {
589        let mut converter = TouchReportConverter::new();
590        let time = zx::MonotonicInstant::from_nanos(100_000_000);
591
592        // contact_id with MSB set (u32::MAX) must be masked to non-negative and never equal -1
593        let report = fir::TouchInputReport {
594            contacts: Some(vec![fir::ContactInputReport {
595                contact_id: Some(u32::MAX),
596                position_x: Some(10),
597                position_y: Some(20),
598                ..Default::default()
599            }]),
600            ..Default::default()
601        };
602        let events = converter.handle(&report, time);
603        let tracking_id_event =
604            events.iter().find(|e| e.code == uapi::ABS_MT_TRACKING_ID as u16).unwrap();
605        assert_eq!(tracking_id_event.value, 0x7fff_ffff);
606        assert!(tracking_id_event.value >= 0);
607        assert_ne!(tracking_id_event.value, LIFTED_TRACKING_ID);
608    }
609
610    #[::fuchsia::test]
611    fn test_touch_report_duplicate_contact_filtered() {
612        let mut converter = TouchReportConverter::new();
613        let time = zx::MonotonicInstant::from_nanos(100_000_000);
614
615        // Report contains duplicate contact_id 1
616        let report = fir::TouchInputReport {
617            contacts: Some(vec![
618                fir::ContactInputReport {
619                    contact_id: Some(1),
620                    position_x: Some(10),
621                    position_y: Some(20),
622                    ..Default::default()
623                },
624                fir::ContactInputReport {
625                    contact_id: Some(1),
626                    position_x: Some(15),
627                    position_y: Some(25),
628                    ..Default::default()
629                },
630            ]),
631            ..Default::default()
632        };
633        let events = converter.handle(&report, time);
634        // Should only have one ABS_MT_SLOT event and one ABS_MT_TRACKING_ID event
635        let slot_count = events.iter().filter(|e| e.code == uapi::ABS_MT_SLOT as u16).count();
636        assert_eq!(slot_count, 1);
637        let tracking_count =
638            events.iter().filter(|e| e.code == uapi::ABS_MT_TRACKING_ID as u16).count();
639        assert_eq!(tracking_count, 1);
640        assert!(events.iter().any(|e| e.code == uapi::ABS_MT_POSITION_X as u16 && e.value == 10));
641        assert!(!events.iter().any(|e| e.code == uapi::ABS_MT_POSITION_X as u16 && e.value == 15));
642    }
643
644    #[::fuchsia::test]
645    fn test_touch_report_missing_coordinates_does_not_lift() {
646        let mut converter = TouchReportConverter::new();
647        let time = zx::MonotonicInstant::from_nanos(100_000_000);
648
649        // Step 1: Initial touch down
650        let report1 = fir::TouchInputReport {
651            contacts: Some(vec![fir::ContactInputReport {
652                contact_id: Some(1),
653                position_x: Some(10),
654                position_y: Some(20),
655                ..Default::default()
656            }]),
657            ..Default::default()
658        };
659        let events1 = converter.handle(&report1, time);
660        assert!(events1.iter().any(|e| e.code == uapi::BTN_TOUCH as u16 && e.value == 1));
661
662        // Step 2: Contact present but missing coordinates
663        let report2 = fir::TouchInputReport {
664            contacts: Some(vec![fir::ContactInputReport {
665                contact_id: Some(1),
666                position_x: None,
667                position_y: None,
668                ..Default::default()
669            }]),
670            ..Default::default()
671        };
672        let events2 = converter.handle(&report2, time);
673        // Must NOT emit lift event for contact 1 or release BTN_TOUCH
674        assert!(
675            !events2.iter().any(|e| e.code == uapi::ABS_MT_TRACKING_ID as u16 && e.value == -1)
676        );
677        assert!(!events2.iter().any(|e| e.code == uapi::BTN_TOUCH as u16 && e.value == 0));
678
679        // Step 3: Truly lifted
680        let report3 = fir::TouchInputReport { contacts: Some(vec![]), ..Default::default() };
681        let events3 = converter.handle(&report3, time);
682        assert!(events3.iter().any(|e| e.code == uapi::ABS_MT_TRACKING_ID as u16 && e.value == -1));
683        assert!(events3.iter().any(|e| e.code == uapi::BTN_TOUCH as u16 && e.value == 0));
684    }
685
686    #[::fuchsia::test]
687    fn test_touch_report_slot_exhaustion_and_reuse() {
688        let mut converter = TouchReportConverter::with_max_slots(2);
689        let time = zx::MonotonicInstant::from_nanos(100_000_000);
690
691        // 3 contacts reported, but max_slots = 2
692        let report1 = fir::TouchInputReport {
693            contacts: Some(vec![
694                fir::ContactInputReport {
695                    contact_id: Some(1),
696                    position_x: Some(10),
697                    position_y: Some(20),
698                    ..Default::default()
699                },
700                fir::ContactInputReport {
701                    contact_id: Some(2),
702                    position_x: Some(30),
703                    position_y: Some(40),
704                    ..Default::default()
705                },
706                fir::ContactInputReport {
707                    contact_id: Some(3),
708                    position_x: Some(50),
709                    position_y: Some(60),
710                    ..Default::default()
711                },
712            ]),
713            ..Default::default()
714        };
715        let events1 = converter.handle(&report1, time);
716        // Contact 1 gets slot 0, contact 2 gets slot 1, contact 3 is dropped
717        assert!(events1.iter().any(|e| e.code == uapi::ABS_MT_SLOT as u16 && e.value == 0));
718        assert!(events1.iter().any(|e| e.code == uapi::ABS_MT_SLOT as u16 && e.value == 1));
719        assert!(!events1.iter().any(|e| e.code == uapi::ABS_MT_SLOT as u16 && e.value == 2));
720        assert!(!events1.iter().any(|e| e.code == uapi::ABS_MT_POSITION_X as u16 && e.value == 50));
721
722        // Contact 1 lifted, contact 2 stays
723        let report2 = fir::TouchInputReport {
724            contacts: Some(vec![fir::ContactInputReport {
725                contact_id: Some(2),
726                position_x: Some(32),
727                position_y: Some(42),
728                ..Default::default()
729            }]),
730            ..Default::default()
731        };
732        let events2 = converter.handle(&report2, time);
733        // Slot 0 (contact 1) is lifted and freed
734        assert!(events2.iter().any(|e| e.code == uapi::ABS_MT_SLOT as u16 && e.value == 0));
735        assert!(events2.iter().any(|e| e.code == uapi::ABS_MT_TRACKING_ID as u16 && e.value == -1));
736
737        // Contact 3 is now reported alongside contact 2
738        let report3 = fir::TouchInputReport {
739            contacts: Some(vec![
740                fir::ContactInputReport {
741                    contact_id: Some(2),
742                    position_x: Some(35),
743                    position_y: Some(45),
744                    ..Default::default()
745                },
746                fir::ContactInputReport {
747                    contact_id: Some(3),
748                    position_x: Some(52),
749                    position_y: Some(62),
750                    ..Default::default()
751                },
752            ]),
753            ..Default::default()
754        };
755        let events3 = converter.handle(&report3, time);
756        // Slot 0 (freed from contact 1) should now be reused for contact 3!
757        assert!(events3.iter().any(|e| e.code == uapi::ABS_MT_SLOT as u16 && e.value == 0));
758        assert!(events3.iter().any(|e| e.code == uapi::ABS_MT_TRACKING_ID as u16 && e.value == 3));
759        assert!(events3.iter().any(|e| e.code == uapi::ABS_MT_POSITION_X as u16 && e.value == 52));
760    }
761
762    #[::fuchsia::test]
763    fn test_empty_touch_reports_return_empty_events() {
764        let time = zx::MonotonicInstant::from_nanos(400_000_000);
765        assert!(
766            TouchReportConverter::new().handle(&fir::TouchInputReport::default(), time).is_empty()
767        );
768    }
769
770    #[::fuchsia::test]
771    fn test_touch_report_clamping_out_of_range_axes() {
772        let mut converter = TouchReportConverter::new();
773        let time = zx::MonotonicInstant::from_nanos(100_000_000);
774        let report = fir::TouchInputReport {
775            contacts: Some(vec![fir::ContactInputReport {
776                contact_id: Some(1),
777                position_x: Some(i64::MAX),
778                position_y: Some(i64::MIN),
779                pressure: Some(i64::MAX),
780                contact_width: Some(i32::MAX as i64 + 100),
781                contact_height: Some(i64::MAX),
782                ..Default::default()
783            }]),
784            ..Default::default()
785        };
786        let events = converter.handle(&report, time);
787        assert!(
788            events.iter().any(|e| e.code == uapi::ABS_MT_POSITION_X as u16 && e.value == i32::MAX)
789        );
790        assert!(
791            events.iter().any(|e| e.code == uapi::ABS_MT_POSITION_Y as u16 && e.value == i32::MIN)
792        );
793        assert!(
794            events.iter().any(|e| e.code == uapi::ABS_MT_PRESSURE as u16 && e.value == i32::MAX)
795        );
796        assert!(
797            events.iter().any(|e| e.code == uapi::ABS_MT_TOUCH_MAJOR as u16 && e.value == i32::MAX)
798        );
799        assert!(
800            events.iter().any(|e| e.code == uapi::ABS_MT_TOUCH_MINOR as u16 && e.value == i32::MAX)
801        );
802    }
803
804    #[::fuchsia::test]
805    fn test_touch_report_deduplicate_unchanged_mt_axes() {
806        let mut converter = TouchReportConverter::new();
807        let time = zx::MonotonicInstant::from_nanos(100_000_000);
808
809        // 1. Initial touch down
810        let report1 = fir::TouchInputReport {
811            contacts: Some(vec![fir::ContactInputReport {
812                contact_id: Some(1),
813                position_x: Some(10),
814                position_y: Some(20),
815                pressure: Some(30),
816                ..Default::default()
817            }]),
818            ..Default::default()
819        };
820        let events1 = converter.handle(&report1, time);
821        assert!(!events1.is_empty());
822        assert!(events1.iter().any(|e| e.code == uapi::ABS_MT_SLOT as u16 && e.value == 0));
823        assert!(events1.iter().any(|e| e.code == uapi::ABS_MT_POSITION_X as u16 && e.value == 10));
824        assert!(events1.iter().any(|e| e.code == uapi::ABS_MT_POSITION_Y as u16 && e.value == 20));
825        assert!(events1.iter().any(|e| e.code == uapi::ABS_MT_PRESSURE as u16 && e.value == 30));
826
827        // 2. Successive scan with unchanged state: should emit zero events!
828        let events2 = converter.handle(&report1, time);
829        assert!(events2.is_empty(), "Unchanged report should emit no events, got: {:?}", events2);
830
831        // 3. Move pos_x only: should emit slot 0 and ABS_MT_POSITION_X only
832        let report3 = fir::TouchInputReport {
833            contacts: Some(vec![fir::ContactInputReport {
834                contact_id: Some(1),
835                position_x: Some(15),
836                position_y: Some(20),
837                pressure: Some(30),
838                ..Default::default()
839            }]),
840            ..Default::default()
841        };
842        let events3 = converter.handle(&report3, time);
843        assert_eq!(
844            events3.iter().map(|e| (e.type_, e.code, e.value)).collect::<Vec<_>>(),
845            vec![
846                (uapi::EV_ABS as u16, uapi::ABS_MT_SLOT as u16, 0),
847                (uapi::EV_ABS as u16, uapi::ABS_MT_POSITION_X as u16, 15),
848                (uapi::EV_SYN as u16, uapi::SYN_REPORT as u16, 0),
849            ]
850        );
851
852        // 4. Change pressure only: should emit slot 0 and ABS_MT_PRESSURE only
853        let report4 = fir::TouchInputReport {
854            contacts: Some(vec![fir::ContactInputReport {
855                contact_id: Some(1),
856                position_x: Some(15),
857                position_y: Some(20),
858                pressure: Some(40),
859                ..Default::default()
860            }]),
861            ..Default::default()
862        };
863        let events4 = converter.handle(&report4, time);
864        assert_eq!(
865            events4.iter().map(|e| (e.type_, e.code, e.value)).collect::<Vec<_>>(),
866            vec![
867                (uapi::EV_ABS as u16, uapi::ABS_MT_SLOT as u16, 0),
868                (uapi::EV_ABS as u16, uapi::ABS_MT_PRESSURE as u16, 40),
869                (uapi::EV_SYN as u16, uapi::SYN_REPORT as u16, 0),
870            ]
871        );
872    }
873
874    #[::fuchsia::test]
875    fn test_touch_report_empty_contacts_with_button_does_not_lift() {
876        let mut converter = TouchReportConverter::new();
877        let time = zx::MonotonicInstant::from_nanos(100_000_000);
878
879        // 1. Initial touch down
880        let report_down = fir::TouchInputReport {
881            contacts: Some(vec![fir::ContactInputReport {
882                contact_id: Some(1),
883                position_x: Some(100),
884                position_y: Some(100),
885                ..Default::default()
886            }]),
887            ..Default::default()
888        };
889        let events1 = converter.handle(&report_down, time);
890        assert!(events1.iter().any(|e| e.code == uapi::BTN_TOUCH as u16 && e.value == 1));
891
892        // 2. Palm button report with empty contacts
893        let report_palm = fir::TouchInputReport {
894            contacts: Some(vec![]),
895            pressed_buttons: Some(vec![fir::TouchButton::Palm]),
896            ..Default::default()
897        };
898        let events2 = converter.handle(&report_palm, time);
899        assert!(events2.iter().any(|e| e.code == uapi::KEY_SLEEP as u16 && e.value == 1));
900        // Contact should NOT be lifted, BTN_TOUCH = 0 should NOT be emitted
901        assert!(
902            !events2.iter().any(|e| e.code == uapi::ABS_MT_TRACKING_ID as u16 && e.value == -1)
903        );
904        assert!(!events2.iter().any(|e| e.code == uapi::BTN_TOUCH as u16 && e.value == 0));
905
906        // 3. Release Palm button with empty contacts
907        let report_release_palm = fir::TouchInputReport {
908            contacts: Some(vec![]),
909            pressed_buttons: Some(vec![]),
910            ..Default::default()
911        };
912        let events3 = converter.handle(&report_release_palm, time);
913        assert!(events3.iter().any(|e| e.code == uapi::KEY_SLEEP as u16 && e.value == 0));
914        // Still should NOT lift contacts because previous report had buttons
915        assert!(
916            !events3.iter().any(|e| e.code == uapi::ABS_MT_TRACKING_ID as u16 && e.value == -1)
917        );
918        assert!(!events3.iter().any(|e| e.code == uapi::BTN_TOUCH as u16 && e.value == 0));
919
920        // 4. Normal lift report (empty contacts, no buttons currently or previously)
921        let report_lift = fir::TouchInputReport {
922            contacts: Some(vec![]),
923            pressed_buttons: None,
924            ..Default::default()
925        };
926        let events4 = converter.handle(&report_lift, time);
927        assert!(events4.iter().any(|e| e.code == uapi::ABS_MT_TRACKING_ID as u16 && e.value == -1));
928        assert!(events4.iter().any(|e| e.code == uapi::BTN_TOUCH as u16 && e.value == 0));
929    }
930
931    #[::fuchsia::test]
932    fn test_touch_report_lift_while_button_remains_held() {
933        let mut converter = TouchReportConverter::new();
934        let time = zx::MonotonicInstant::from_nanos(100_000_000);
935
936        // 1. Contact down + Palm button pressed
937        let report_down = fir::TouchInputReport {
938            contacts: Some(vec![fir::ContactInputReport {
939                contact_id: Some(1),
940                position_x: Some(100),
941                position_y: Some(100),
942                ..Default::default()
943            }]),
944            pressed_buttons: Some(vec![fir::TouchButton::Palm]),
945            ..Default::default()
946        };
947        let events1 = converter.handle(&report_down, time);
948        assert!(events1.iter().any(|e| e.code == uapi::BTN_TOUCH as u16 && e.value == 1));
949        assert!(events1.iter().any(|e| e.code == uapi::KEY_SLEEP as u16 && e.value == 1));
950
951        // 2. Finger lifted while Palm button is still held
952        let report_lift = fir::TouchInputReport {
953            contacts: Some(vec![]),
954            pressed_buttons: Some(vec![fir::TouchButton::Palm]),
955            ..Default::default()
956        };
957        let events2 = converter.handle(&report_lift, time);
958        // Contact should be lifted even though Palm button is still pressed
959        assert!(events2.iter().any(|e| e.code == uapi::ABS_MT_TRACKING_ID as u16 && e.value == -1));
960        assert!(events2.iter().any(|e| e.code == uapi::BTN_TOUCH as u16 && e.value == 0));
961        // Palm button state didn't change, so no KEY_SLEEP event
962        assert!(!events2.iter().any(|e| e.code == uapi::KEY_SLEEP as u16));
963    }
964
965    #[::fuchsia::test]
966    fn test_touch_report_lift_with_none_pressed_buttons_after_button_pressed() {
967        let mut converter = TouchReportConverter::new();
968        let time = zx::MonotonicInstant::from_nanos(100_000_000);
969
970        // 1. Contact down + Palm button pressed
971        let report_down = fir::TouchInputReport {
972            contacts: Some(vec![fir::ContactInputReport {
973                contact_id: Some(1),
974                position_x: Some(100),
975                position_y: Some(100),
976                ..Default::default()
977            }]),
978            pressed_buttons: Some(vec![fir::TouchButton::Palm]),
979            ..Default::default()
980        };
981        let events1 = converter.handle(&report_down, time);
982        assert!(events1.iter().any(|e| e.code == uapi::BTN_TOUCH as u16 && e.value == 1));
983        assert!(events1.iter().any(|e| e.code == uapi::KEY_SLEEP as u16 && e.value == 1));
984
985        // 2. Contact lifted with pressed_buttons: None
986        let report_lift = fir::TouchInputReport {
987            contacts: Some(vec![]),
988            pressed_buttons: None,
989            ..Default::default()
990        };
991        let events2 = converter.handle(&report_lift, time);
992        // Contact should be lifted
993        assert!(events2.iter().any(|e| e.code == uapi::ABS_MT_TRACKING_ID as u16 && e.value == -1));
994        assert!(events2.iter().any(|e| e.code == uapi::BTN_TOUCH as u16 && e.value == 0));
995    }
996
997    #[::fuchsia::test]
998    fn test_touch_report_omitted_optional_axes_preserve_cached_values() {
999        let mut converter = TouchReportConverter::new();
1000        let time = zx::MonotonicInstant::from_nanos(100_000_000);
1001
1002        // 1. Initial touch down with pressure and dimensions
1003        let report1 = fir::TouchInputReport {
1004            contacts: Some(vec![fir::ContactInputReport {
1005                contact_id: Some(1),
1006                position_x: Some(10),
1007                position_y: Some(20),
1008                pressure: Some(40),
1009                contact_width: Some(30),
1010                contact_height: Some(15),
1011                ..Default::default()
1012            }]),
1013            ..Default::default()
1014        };
1015        let events1 = converter.handle(&report1, time);
1016        assert!(events1.iter().any(|e| e.code == uapi::ABS_MT_PRESSURE as u16 && e.value == 40));
1017        assert!(events1.iter().any(|e| e.code == uapi::ABS_MT_TOUCH_MAJOR as u16 && e.value == 30));
1018        assert!(events1.iter().any(|e| e.code == uapi::ABS_MT_TOUCH_MINOR as u16 && e.value == 15));
1019
1020        // 2. Report with unchanged coordinates and omitted optional axes (None)
1021        // Should NOT emit events (no spurious slot + syn frame)
1022        let report2 = fir::TouchInputReport {
1023            contacts: Some(vec![fir::ContactInputReport {
1024                contact_id: Some(1),
1025                position_x: Some(10),
1026                position_y: Some(20),
1027                pressure: None,
1028                contact_width: None,
1029                contact_height: None,
1030                ..Default::default()
1031            }]),
1032            ..Default::default()
1033        };
1034        let events2 = converter.handle(&report2, time);
1035        assert!(
1036            events2.is_empty(),
1037            "Expected no events when optional axes are omitted and coords unchanged, got: {:?}",
1038            events2
1039        );
1040
1041        // 3. Move position_x with omitted optional axes: should emit slot + position_x only,
1042        // and retain cached pressure/dimensions.
1043        let report3 = fir::TouchInputReport {
1044            contacts: Some(vec![fir::ContactInputReport {
1045                contact_id: Some(1),
1046                position_x: Some(15),
1047                position_y: Some(20),
1048                pressure: None,
1049                contact_width: None,
1050                contact_height: None,
1051                ..Default::default()
1052            }]),
1053            ..Default::default()
1054        };
1055        let events3 = converter.handle(&report3, time);
1056        assert_eq!(
1057            events3.iter().map(|e| (e.type_, e.code, e.value)).collect::<Vec<_>>(),
1058            vec![
1059                (uapi::EV_ABS as u16, uapi::ABS_MT_SLOT as u16, 0),
1060                (uapi::EV_ABS as u16, uapi::ABS_MT_POSITION_X as u16, 15),
1061                (uapi::EV_SYN as u16, uapi::SYN_REPORT as u16, 0),
1062            ]
1063        );
1064
1065        // 4. Report with same position_x: 15 and pressure: Some(40), dimensions: Some(30, 15).
1066        // Since cached state was preserved, these values match cache and emit nothing!
1067        let report4 = fir::TouchInputReport {
1068            contacts: Some(vec![fir::ContactInputReport {
1069                contact_id: Some(1),
1070                position_x: Some(15),
1071                position_y: Some(20),
1072                pressure: Some(40),
1073                contact_width: Some(30),
1074                contact_height: Some(15),
1075                ..Default::default()
1076            }]),
1077            ..Default::default()
1078        };
1079        let events4 = converter.handle(&report4, time);
1080        assert!(
1081            events4.is_empty(),
1082            "Expected no events when reporting matching cached optional axes, got: {:?}",
1083            events4
1084        );
1085    }
1086
1087    #[::fuchsia::test]
1088    fn test_touch_report_converter_from_descriptors() {
1089        // From TouchInputDescriptor with max_contacts
1090        let input_desc1 = fir::TouchInputDescriptor {
1091            max_contacts: Some(5),
1092            contacts: Some(vec![fir::ContactInputDescriptor::default(); 2]),
1093            ..Default::default()
1094        };
1095        let converter1 = TouchReportConverter::from_touch_input_descriptor(&input_desc1);
1096        assert_eq!(converter1.max_slots(), 5);
1097
1098        // From TouchInputDescriptor with contacts len fallback
1099        let input_desc2 = fir::TouchInputDescriptor {
1100            max_contacts: None,
1101            contacts: Some(vec![fir::ContactInputDescriptor::default(); 3]),
1102            ..Default::default()
1103        };
1104        let converter2 = TouchReportConverter::from_touch_input_descriptor(&input_desc2);
1105        assert_eq!(converter2.max_slots(), 3);
1106
1107        // From TouchInputDescriptor with max_contacts = 0 (clamped to 1)
1108        let input_desc_zero = fir::TouchInputDescriptor {
1109            max_contacts: Some(0),
1110            contacts: None,
1111            ..Default::default()
1112        };
1113        let converter_zero = TouchReportConverter::from_touch_input_descriptor(&input_desc_zero);
1114        assert_eq!(converter_zero.max_slots(), 1);
1115
1116        // From TouchInputDescriptor with all None (defaults to DEFAULT_MAX_TOUCH_CONTACTS)
1117        let input_desc_default = fir::TouchInputDescriptor::default();
1118        let converter_default =
1119            TouchReportConverter::from_touch_input_descriptor(&input_desc_default);
1120        assert_eq!(converter_default.max_slots(), DEFAULT_MAX_TOUCH_CONTACTS);
1121
1122        // From TouchDescriptor with input
1123        let touch_desc = fir::TouchDescriptor {
1124            input: Some(fir::TouchInputDescriptor { max_contacts: Some(8), ..Default::default() }),
1125            ..Default::default()
1126        };
1127        let converter3 = TouchReportConverter::from_touch_descriptor(&touch_desc);
1128        assert_eq!(converter3.max_slots(), 8);
1129
1130        // From TouchDescriptor with None input
1131        let touch_desc_none = fir::TouchDescriptor::default();
1132        let converter4 = TouchReportConverter::from_touch_descriptor(&touch_desc_none);
1133        assert_eq!(converter4.max_slots(), DEFAULT_MAX_TOUCH_CONTACTS);
1134    }
1135}