session_manager_lib/
debug.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 fuchsia_sync::Mutex;
6use futures::StreamExt;
7use log::{debug, info, warn};
8use std::sync::{Arc, LazyLock};
9use zx;
10
11use fidl::endpoints::create_request_stream;
12use fidl_fuchsia_ui_input::MediaButtonsEvent;
13use fidl_fuchsia_ui_policy as fuipolicy;
14
15static MAX_PRESS_INTERVAL_NS: LazyLock<i64> = LazyLock::new(|| 500 * 1_000_000); // 500ms in nanoseconds
16const REQUIRED_PRESS_COUNT: u32 = 5;
17
18pub trait Clock: Send + Sync {
19    fn now_ns(&self) -> i64;
20}
21
22pub struct BootClock;
23
24impl Clock for BootClock {
25    fn now_ns(&self) -> i64 {
26        zx::BootInstant::get().into_nanos()
27    }
28}
29
30#[derive(Debug)]
31struct ButtonPressState {
32    count: u32,
33    last_press_time_ns: i64,
34    power_was_pressed: bool,
35    #[cfg(test)]
36    action_triggered_count: u32,
37}
38
39impl ButtonPressState {
40    fn new() -> Self {
41        Self {
42            count: 0,
43            last_press_time_ns: 0,
44            power_was_pressed: false,
45            #[cfg(test)]
46            action_triggered_count: 0,
47        }
48    }
49}
50
51pub struct DebugState<C: Clock> {
52    /// Whether the system supports 5-button press to debug.
53    debug_enabled: bool,
54    button_press_state: Mutex<ButtonPressState>,
55    clock: C,
56}
57
58/// The concrete `DebugState` used in production.
59pub type DebugManager = DebugState<BootClock>;
60
61impl DebugState<BootClock> {
62    pub fn new(debug_enabled: bool) -> Self {
63        Self {
64            debug_enabled,
65            button_press_state: Mutex::new(ButtonPressState::new()),
66            clock: BootClock,
67        }
68    }
69}
70
71impl<C: Clock + 'static> DebugState<C> {
72    #[cfg(test)]
73    fn new_for_test(debug_enabled: bool, clock: C) -> Self {
74        Self { debug_enabled, button_press_state: Mutex::new(ButtonPressState::new()), clock }
75    }
76
77    pub fn start_media_buttons_listener(self: Arc<Self>) {
78        if !self.debug_enabled {
79            debug!("Debug mode not enabled, skipping media button listener registration.");
80            return;
81        }
82
83        info!("Registering for media button events to enable 5-button press for debug.");
84        fuchsia_async::Task::spawn(async move {
85            match fuchsia_component::client::connect_to_protocol::<
86                fuipolicy::DeviceListenerRegistryMarker,
87            >() {
88                Ok(device_listener_registry) => {
89                    let (client_end, mut stream) =
90                        create_request_stream::<fuipolicy::MediaButtonsListenerMarker>();
91                    if let Err(e) = device_listener_registry.register_listener(client_end).await {
92                        warn!("Failed to register media buttons listener: {e:?}");
93                        return;
94                    }
95                    while let Some(Ok(request)) = stream.next().await {
96                        if let fuipolicy::MediaButtonsListenerRequest::OnEvent {
97                            event,
98                            responder,
99                        } = request
100                        {
101                            if self.process_button_event(&event) {
102                                // TODO: b/475927005 - Trigger a crash report and system reboot.
103                                debug!("Detected 5 function button presses in a row.");
104                            }
105                            if let Err(e) = responder.send() {
106                                warn!("Failed to send response for media buttons event: {e:?}");
107                            }
108                        }
109                    }
110                }
111                Err(e) => {
112                    warn!("Failed to connect to fuchsia.ui.policy.DeviceListenerRegistry: {e:?}");
113                }
114            }
115        })
116        .detach();
117    }
118
119    fn process_button_event(&self, event: &MediaButtonsEvent) -> bool {
120        let mut state = self.button_press_state.lock();
121        if let Some(power_is_pressed) = event.power
122            && (power_is_pressed || state.power_was_pressed)
123        {
124            debug!("Detected overlapping POWER button activity; resetting FUNCTION button counter");
125            state.power_was_pressed = power_is_pressed;
126            state.count = 0;
127            return false;
128        }
129
130        if event.function != Some(true) {
131            // Function button could have been released. Ignore it.
132            return false;
133        }
134
135        // At this point, we have a pure function press event.
136        let now_ns = self.clock.now_ns();
137
138        if now_ns - state.last_press_time_ns > *MAX_PRESS_INTERVAL_NS {
139            state.count = 1;
140        } else {
141            state.count += 1;
142        }
143
144        state.last_press_time_ns = now_ns;
145
146        if state.count >= REQUIRED_PRESS_COUNT {
147            state.count = 0;
148            #[cfg(test)]
149            {
150                state.action_triggered_count += 1;
151            }
152            return true;
153        }
154        false
155    }
156
157    pub fn stop(&self) {
158        if !self.debug_enabled {
159            return;
160        }
161        debug!("Resetting debug button press state.");
162        *self.button_press_state.lock() = ButtonPressState::new();
163    }
164
165    #[cfg(test)]
166    pub(crate) fn get_button_press_state_count(&self) -> u32 {
167        self.button_press_state.lock().count
168    }
169
170    #[cfg(test)]
171    pub(crate) fn set_button_press_state_count(&self, count: u32) {
172        self.button_press_state.lock().count = count;
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use fidl_fuchsia_ui_input as fui_input;
180
181    struct FakeClock {
182        now: Mutex<i64>,
183    }
184
185    impl FakeClock {
186        fn new() -> Self {
187            Self { now: Mutex::new(0) }
188        }
189
190        fn advance_ns(&self, duration_ns: i64) {
191            *self.now.lock() += duration_ns;
192        }
193    }
194
195    impl Clock for FakeClock {
196        fn now_ns(&self) -> i64 {
197            *self.now.lock()
198        }
199    }
200
201    // By implementing Clock for Arc<T>, we can pass a clone of the Arc to the DebugState,
202    // while the test retains ownership of the original Arc. This allows the test to call
203    // `advance_ns` on the FakeClock.
204    impl<T: Clock> Clock for Arc<T> {
205        fn now_ns(&self) -> i64 {
206            self.as_ref().now_ns()
207        }
208    }
209
210    #[fuchsia::test]
211    fn test_successful_press_sequence() {
212        let clock = Arc::new(FakeClock::new());
213        let debug_state = DebugState::new_for_test(true, clock.clone());
214        let press_event =
215            fui_input::MediaButtonsEvent { function: Some(true), ..Default::default() };
216
217        // Press the button REQUIRED_PRESS_COUNT - 1 times, which should not trigger the debug state.
218        for i in 1..REQUIRED_PRESS_COUNT {
219            assert!(
220                !debug_state.process_button_event(&press_event),
221                "Incorrectly triggered action after {i} presses"
222            );
223            let state = debug_state.button_press_state.lock();
224            assert_eq!(
225                state.action_triggered_count, 0,
226                "Incorrectly incremented trigger count after {i} presses. State: {state:?}"
227            );
228        }
229
230        assert!(
231            debug_state.process_button_event(&press_event),
232            "The final press should trigger the sequence."
233        );
234        let state = debug_state.button_press_state.lock();
235        assert_eq!(state.action_triggered_count, 1, "State: {state:?}");
236    }
237
238    #[fuchsia::test]
239    fn test_counter_resets_after_successful_sequence() {
240        let clock = Arc::new(FakeClock::new());
241        let debug_state = DebugState::new_for_test(true, clock.clone());
242        let press_event =
243            fui_input::MediaButtonsEvent { function: Some(true), ..Default::default() };
244
245        // Test that the counter resets after a successful sequence.
246        for _ in 1..REQUIRED_PRESS_COUNT {
247            assert!(!debug_state.process_button_event(&press_event));
248            let state = debug_state.button_press_state.lock();
249            assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
250        }
251        assert!(debug_state.process_button_event(&press_event));
252        {
253            let state = debug_state.button_press_state.lock();
254            assert_eq!(state.action_triggered_count, 1, "State: {state:?}");
255        }
256
257        assert!(
258            !debug_state.process_button_event(&press_event),
259            "The next press should start a new sequence."
260        );
261        let state = debug_state.button_press_state.lock();
262        assert_eq!(state.count, 1, "State: {state:?}");
263        assert_eq!(state.action_triggered_count, 1, "State: {state:?}");
264    }
265
266    #[test]
267    fn test_counter_resets_after_timeout() {
268        let clock = Arc::new(FakeClock::new());
269        let debug_state = DebugState::new_for_test(true, clock.clone());
270        let press_event =
271            fui_input::MediaButtonsEvent { function: Some(true), ..Default::default() };
272
273        // Test that the counter resets after a timeout.
274        for _ in 1..REQUIRED_PRESS_COUNT - 1 {
275            assert!(!debug_state.process_button_event(&press_event));
276            let state = debug_state.button_press_state.lock();
277            assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
278        }
279        // Wait for longer than the max interval.
280        clock.advance_ns(*MAX_PRESS_INTERVAL_NS + 1);
281
282        assert!(
283            !debug_state.process_button_event(&press_event),
284            "This press should reset the counter to 1, not increment it."
285        );
286        let state = debug_state.button_press_state.lock();
287        assert_eq!(state.count, 1, "State: {state:?}");
288        assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
289    }
290
291    #[fuchsia::test]
292    fn test_power_button_press_resets_counter() {
293        let clock = Arc::new(FakeClock::new());
294        let debug_state = DebugState::new_for_test(true, clock.clone());
295        let press_event =
296            fui_input::MediaButtonsEvent { function: Some(true), ..Default::default() };
297        let power_press_event =
298            fui_input::MediaButtonsEvent { power: Some(true), ..Default::default() };
299
300        assert!(
301            !debug_state.process_button_event(&press_event),
302            "first function press should not trigger action"
303        );
304        {
305            let state = debug_state.button_press_state.lock();
306            assert_eq!(state.count, 1, "should have count of 1. State: {state:?}");
307        }
308        assert!(
309            !debug_state.process_button_event(&power_press_event),
310            "power press should not trigger action"
311        );
312        {
313            let state = debug_state.button_press_state.lock();
314            assert_eq!(
315                state.count, 0,
316                "A non-function-button press should reset the counter. State: {state:?}"
317            );
318            assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
319        }
320
321        assert!(
322            !debug_state.process_button_event(&press_event),
323            "The next press should start a new sequence."
324        );
325        let state = debug_state.button_press_state.lock();
326        assert_eq!(state.count, 1, "State: {state:?}");
327        assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
328    }
329
330    #[fuchsia::test]
331    fn test_power_button_release_resets_counter() {
332        let clock = Arc::new(FakeClock::new());
333        let debug_state = DebugState::new_for_test(true, clock.clone());
334        let function_press_event =
335            fui_input::MediaButtonsEvent { function: Some(true), ..Default::default() };
336        let function_release_event =
337            fui_input::MediaButtonsEvent { function: Some(false), ..Default::default() };
338        let power_press_event =
339            fui_input::MediaButtonsEvent { power: Some(true), ..Default::default() };
340        let power_release_event =
341            fui_input::MediaButtonsEvent { power: Some(false), ..Default::default() };
342
343        assert!(
344            !debug_state.process_button_event(&power_press_event),
345            "power press should not trigger action"
346        );
347        {
348            let state = debug_state.button_press_state.lock();
349            assert_eq!(state.count, 0, "count should be 0 after power press. State: {state:?}");
350        }
351        assert!(
352            !debug_state.process_button_event(&function_press_event),
353            "function press should not trigger action"
354        );
355        {
356            let state = debug_state.button_press_state.lock();
357            assert_eq!(state.count, 1, "count should be 1 after function press. State: {state:?}");
358        }
359        assert!(
360            !debug_state.process_button_event(&power_release_event),
361            "A non-function-button release should reset the counter."
362        );
363        {
364            let state = debug_state.button_press_state.lock();
365            assert_eq!(state.count, 0, "count should be 0 after power release. State: {state:?}");
366            assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
367        }
368        assert!(
369            !debug_state.process_button_event(&function_release_event),
370            "function release should not trigger action"
371        );
372        {
373            let state = debug_state.button_press_state.lock();
374            assert_eq!(
375                state.count, 0,
376                "count should be 0 after function release. State: {state:?}"
377            );
378            assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
379        }
380
381        assert!(
382            !debug_state.process_button_event(&function_press_event),
383            "The next press should start a new sequence."
384        );
385        let state = debug_state.button_press_state.lock();
386        assert_eq!(state.count, 1, "State: {state:?}");
387        assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
388    }
389
390    #[fuchsia::test]
391    fn test_ignores_function_button_releases() {
392        let clock = Arc::new(FakeClock::new());
393        let debug_state = DebugState::new_for_test(true, clock.clone());
394        let press_event =
395            fui_input::MediaButtonsEvent { function: Some(true), ..Default::default() };
396        let function_release_event =
397            fui_input::MediaButtonsEvent { function: Some(false), ..Default::default() };
398
399        assert!(
400            !debug_state.process_button_event(&press_event),
401            "first function press should not trigger action"
402        );
403        {
404            let state = debug_state.button_press_state.lock();
405            assert_eq!(state.count, 1, "count should be 1. State: {state:?}");
406        }
407        assert!(
408            !debug_state.process_button_event(&function_release_event),
409            "A button release should be ignored and not affect the counter."
410        );
411        {
412            let state = debug_state.button_press_state.lock();
413            assert_eq!(state.count, 1, "count should still be 1. State: {state:?}");
414            assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
415        }
416        assert!(
417            !debug_state.process_button_event(&press_event),
418            "second function press should not trigger action"
419        );
420        let state = debug_state.button_press_state.lock();
421        assert_eq!(state.count, 2, "count should be 2. State: {state:?}");
422        assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
423    }
424}