1use 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_feedback as ffeedback;
13use fidl_fuchsia_hardware_power_statecontrol as fpower;
14use fidl_fuchsia_ui_input::MediaButtonsEvent;
15use fidl_fuchsia_ui_policy as fuipolicy;
16
17static MAX_PRESS_INTERVAL_NS: LazyLock<i64> = LazyLock::new(|| 500 * 1_000_000); const REQUIRED_PRESS_COUNT: u32 = 5;
19
20pub trait Clock: Send + Sync {
21 fn now_ns(&self) -> i64;
22}
23
24pub struct BootClock;
25
26impl Clock for BootClock {
27 fn now_ns(&self) -> i64 {
28 zx::BootInstant::get().into_nanos()
29 }
30}
31
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
33struct CurrentButtons {
34 power_pressed: bool,
35 volume_up_pressed: bool,
36 volume_down_pressed: bool,
37}
38
39impl CurrentButtons {
40 fn both_volume_pressed(self) -> bool {
41 self.volume_up_pressed && self.volume_down_pressed
42 }
43}
44
45#[derive(Debug)]
46struct ButtonPressState {
47 count: u32,
48 last_press_time_ns: i64,
49 buttons: CurrentButtons,
50 crash_report_in_progress: bool,
51 #[cfg(test)]
52 action_triggered_count: u32,
53}
54
55impl ButtonPressState {
56 fn new() -> Self {
57 Self {
58 count: 0,
59 last_press_time_ns: 0,
60 buttons: CurrentButtons::default(),
61 crash_report_in_progress: false,
62 #[cfg(test)]
63 action_triggered_count: 0,
64 }
65 }
66}
67
68pub struct DebugState<C: Clock> {
69 debug_enabled: bool,
71 button_press_state: Mutex<ButtonPressState>,
72 clock: C,
73 crash_reporter_source: Option<ffeedback::CrashReporterProxy>,
74 power_statecontrol_admin_source: Option<fpower::AdminProxy>,
75 listener_task: Mutex<Option<fuchsia_async::Task<()>>>,
76}
77
78pub type DebugManager = DebugState<BootClock>;
80
81impl DebugState<BootClock> {
82 pub fn new(debug_enabled: bool) -> Self {
83 Self {
84 debug_enabled,
85 button_press_state: Mutex::new(ButtonPressState::new()),
86 clock: BootClock,
87 crash_reporter_source: fuchsia_component::client::connect_to_protocol::<
88 ffeedback::CrashReporterMarker,
89 >()
90 .map_err(|e| warn!("Failed to connect to CrashReporter: {e}"))
91 .ok(),
92 power_statecontrol_admin_source: fuchsia_component::client::connect_to_protocol::<
93 fpower::AdminMarker,
94 >()
95 .map_err(|e| warn!("Failed to connect to PowerStateControlAdmin: {e}"))
96 .ok(),
97 listener_task: Mutex::new(None),
98 }
99 }
100}
101
102impl<C: Clock + 'static> DebugState<C> {
103 #[cfg(test)]
104 fn new_for_test(
105 debug_enabled: bool,
106 clock: C,
107 crash_reporter_proxy: Option<ffeedback::CrashReporterProxy>,
108 power_statecontrol_admin_proxy: Option<fpower::AdminProxy>,
109 ) -> Self {
110 Self {
111 debug_enabled,
112 button_press_state: Mutex::new(ButtonPressState::new()),
113 clock,
114 crash_reporter_source: crash_reporter_proxy,
115 power_statecontrol_admin_source: power_statecontrol_admin_proxy,
116 listener_task: Mutex::new(None),
117 }
118 }
119
120 pub fn start_media_buttons_listener(self: Arc<Self>) {
121 if !self.debug_enabled {
122 info!("Debug mode not enabled, skipping media button listener registration.");
123 return;
124 }
125
126 let mut task_lock = self.listener_task.lock();
127 if task_lock.is_some() {
128 return;
130 }
131
132 info!("Registering for media button events to enable 5-button press for debug.");
133 let weak_this = Arc::downgrade(&self);
134 let task = fuchsia_async::Task::spawn(async move {
135 match fuchsia_component::client::connect_to_protocol::<
136 fuipolicy::DeviceListenerRegistryMarker,
137 >() {
138 Ok(device_listener_registry) => {
139 let (client_end, mut stream) =
140 create_request_stream::<fuipolicy::MediaButtonsListenerMarker>();
141 if let Err(e) = device_listener_registry.register_listener(client_end).await {
142 warn!("Failed to register media buttons listener: {e:?}");
143 return;
144 }
145 while let Some(Ok(request)) = stream.next().await {
146 if let fuipolicy::MediaButtonsListenerRequest::OnEvent {
147 event,
148 responder,
149 } = request
150 {
151 if let Some(this) = weak_this.upgrade() {
152 if this.process_button_event(&event) {
153 info!(
154 "Detected 5 volume up+down button presses in a row. Filing crash report."
155 );
156 if this.file_crash_report().await {
157 if !this.reboot_device().await {
158 this.button_press_state
159 .lock()
160 .crash_report_in_progress = false;
161 }
162 } else {
163 this.button_press_state.lock().crash_report_in_progress =
164 false;
165 }
166 }
167 } else {
168 break;
170 }
171 if let Err(e) = responder.send() {
172 warn!("Failed to send response for media buttons event: {e:?}");
173 }
174 }
175 }
176 }
177 Err(e) => {
178 warn!("Failed to connect to fuchsia.ui.policy.DeviceListenerRegistry: {e:?}");
179 }
180 }
181 });
182 *task_lock = Some(task);
183 }
184
185 fn process_button_event(&self, event: &MediaButtonsEvent) -> bool {
186 let mut state = self.button_press_state.lock();
187 let was_both_pressed = state.buttons.both_volume_pressed();
191
192 if let Some(v_up) = event.volume_up {
193 state.buttons.volume_up_pressed = v_up;
194 }
195 if let Some(v_down) = event.volume_down {
196 state.buttons.volume_down_pressed = v_down;
197 }
198
199 let both_volume_pressed = state.buttons.both_volume_pressed();
200 let is_new_press = both_volume_pressed && !was_both_pressed;
201
202 if let Some(power_is_pressed) = event.power
203 && (power_is_pressed || state.buttons.power_pressed)
204 {
205 if state.count >= 3 {
206 info!(
207 "Detected overlapping POWER button activity; resetting VOLUME button counter."
208 );
209 } else {
210 debug!(
211 "Detected overlapping POWER button activity; resetting VOLUME button counter."
212 );
213 }
214 state.buttons.power_pressed = power_is_pressed;
215 state.count = 0;
216 return false;
217 }
218
219 if state.crash_report_in_progress {
220 info!("Crash report in progress, ignoring button event.");
221 return false;
222 }
223
224 if !is_new_press {
225 return false;
226 }
227
228 let now_ns = self.clock.now_ns();
230
231 if now_ns - state.last_press_time_ns > *MAX_PRESS_INTERVAL_NS {
232 state.count = 1;
233 } else {
234 state.count += 1;
235 }
236
237 state.last_press_time_ns = now_ns;
238
239 if state.count >= 3 {
240 info!(
241 "Identified {:?} volume up+down button presses in a row. {:?} in a row will trigger a crash report and reboot sequence.",
242 state.count, REQUIRED_PRESS_COUNT
243 );
244 }
245
246 if state.count >= REQUIRED_PRESS_COUNT {
247 state.count = 0;
248 state.crash_report_in_progress = true;
249 #[cfg(test)]
250 {
251 state.action_triggered_count += 1;
252 }
253 return true;
254 }
255 false
256 }
257
258 pub fn stop(&self) {
259 if !self.debug_enabled {
260 return;
261 }
262 info!("Resetting debug button press state.");
263 *self.button_press_state.lock() = ButtonPressState::new();
264 }
265
266 #[cfg(test)]
267 pub(crate) fn get_button_press_state_count(&self) -> u32 {
268 self.button_press_state.lock().count
269 }
270
271 #[cfg(test)]
272 pub(crate) fn set_button_press_state_count(&self, count: u32) {
273 self.button_press_state.lock().count = count;
274 }
275
276 async fn file_crash_report(&self) -> bool {
277 if let Some(reporter) = &self.crash_reporter_source {
278 let report = ffeedback::CrashReport {
279 program_name: Some("session_manager".to_string()),
280 crash_signature: Some("fuchsia-user-SOS-device-stuck".to_string()),
281 is_fatal: Some(false),
282 ..Default::default()
283 };
284 match reporter.file_report(report).await {
285 Ok(Ok(_)) => {
286 info!("Successfully filed crash report.");
287 true
288 }
289 Ok(Err(e)) => {
290 warn!("Failed to file crash report: {e:?}");
291 false
292 }
293 Err(e) => {
294 warn!("Failed to file crash report: {e:?}");
295 false
296 }
297 }
298 } else {
299 warn!("Failed to get fuchsia.feedback.CrashReporter");
300 false
301 }
302 }
303
304 async fn reboot_device(&self) -> bool {
305 info!("Rebooting device due to 5-button press.");
306 if let Some(proxy) = self.power_statecontrol_admin_source.clone() {
307 let options = fpower::ShutdownOptions {
308 action: Some(fpower::ShutdownAction::Reboot),
309 reasons: Some(vec![fpower::ShutdownReason::UserRequestDeviceStuck]),
310 ..Default::default()
311 };
312 if let Err(e) = proxy.shutdown(&options).await {
313 warn!("Failed to reboot device: {e:?}");
314 false
315 } else {
316 true
317 }
318 } else {
319 warn!("Failed to connect to fuchsia.hardware.power.statecontrol.Admin");
320 false
321 }
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use fidl::endpoints::create_proxy_and_stream;
329 use fidl_fuchsia_ui_input as fui_input;
330 use futures::{TryStreamExt, join};
331
332 struct FakeClock {
333 now: Mutex<i64>,
334 }
335
336 impl FakeClock {
337 fn new() -> Self {
338 Self { now: Mutex::new(0) }
339 }
340
341 fn advance_ns(&self, duration_ns: i64) {
342 *self.now.lock() += duration_ns;
343 }
344 }
345
346 impl Clock for FakeClock {
347 fn now_ns(&self) -> i64 {
348 *self.now.lock()
349 }
350 }
351
352 impl<T: Clock> Clock for Arc<T> {
356 fn now_ns(&self) -> i64 {
357 self.as_ref().now_ns()
358 }
359 }
360
361 fn make_both_press_event() -> fui_input::MediaButtonsEvent {
362 fui_input::MediaButtonsEvent {
363 volume_up: Some(true),
364 volume_down: Some(true),
365 ..Default::default()
366 }
367 }
368
369 fn make_release_event() -> fui_input::MediaButtonsEvent {
370 fui_input::MediaButtonsEvent {
371 volume_up: Some(false),
372 volume_down: Some(false),
373 ..Default::default()
374 }
375 }
376
377 #[fuchsia::test]
378 fn test_successful_simultaneous_press_sequence() {
379 let clock = Arc::new(FakeClock::new());
380 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
381
382 let press_event = make_both_press_event();
383 let release_event = make_release_event();
384
385 for i in 1..REQUIRED_PRESS_COUNT {
387 assert!(
388 !debug_state.process_button_event(&press_event),
389 "Incorrectly triggered action after {i} presses"
390 );
391 let state = debug_state.button_press_state.lock();
392 assert_eq!(state.count, i);
393 assert_eq!(
394 state.action_triggered_count, 0,
395 "Incorrectly incremented trigger count after {i} presses. State: {state:?}"
396 );
397 drop(state);
398
399 assert!(!debug_state.process_button_event(&release_event));
400 }
401
402 assert!(
403 debug_state.process_button_event(&press_event),
404 "The final press should trigger the sequence."
405 );
406 let state = debug_state.button_press_state.lock();
407 assert_eq!(state.action_triggered_count, 1, "State: {state:?}");
408 }
409
410 #[fuchsia::test]
411 fn test_staggered_press_triggers() {
412 let clock = Arc::new(FakeClock::new());
413 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
414
415 let vol_up_event =
416 fui_input::MediaButtonsEvent { volume_up: Some(true), ..Default::default() };
417 let vol_down_event =
418 fui_input::MediaButtonsEvent { volume_down: Some(true), ..Default::default() };
419 let release_event = make_release_event();
420
421 for i in 1..REQUIRED_PRESS_COUNT {
422 assert!(!debug_state.process_button_event(&vol_up_event));
423 assert!(!debug_state.process_button_event(&vol_down_event));
424 let state = debug_state.button_press_state.lock();
425 assert_eq!(state.count, i);
426 drop(state);
427
428 assert!(!debug_state.process_button_event(&release_event));
429 }
430
431 assert!(!debug_state.process_button_event(&vol_up_event));
432 assert!(debug_state.process_button_event(&vol_down_event));
433 let state = debug_state.button_press_state.lock();
434 assert_eq!(state.action_triggered_count, 1);
435 }
436
437 #[fuchsia::test]
438 fn test_holding_both_buttons_does_not_repeat_count() {
439 let clock = Arc::new(FakeClock::new());
440 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
441
442 let press_event = make_both_press_event();
443
444 assert!(!debug_state.process_button_event(&press_event));
446 assert_eq!(debug_state.button_press_state.lock().count, 1);
447
448 assert!(!debug_state.process_button_event(&press_event));
450 assert_eq!(debug_state.button_press_state.lock().count, 1);
451 assert!(!debug_state.process_button_event(&press_event));
452 assert_eq!(debug_state.button_press_state.lock().count, 1);
453 }
454
455 #[fuchsia::test]
456 fn test_counter_resets_after_successful_sequence() {
457 let clock = Arc::new(FakeClock::new());
458 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
459
460 let press_event = make_both_press_event();
461 let release_event = make_release_event();
462
463 for _ in 1..REQUIRED_PRESS_COUNT {
465 assert!(!debug_state.process_button_event(&press_event));
466 let state = debug_state.button_press_state.lock();
467 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
468 drop(state);
469 assert!(!debug_state.process_button_event(&release_event));
470 }
471 assert!(debug_state.process_button_event(&press_event));
472 {
473 let mut state = debug_state.button_press_state.lock();
474 assert_eq!(state.action_triggered_count, 1, "State: {state:?}");
475 assert!(state.crash_report_in_progress);
476 state.crash_report_in_progress = false;
478 }
479
480 assert!(!debug_state.process_button_event(&release_event));
481 assert!(
482 !debug_state.process_button_event(&press_event),
483 "The next press should start a new sequence."
484 );
485 let state = debug_state.button_press_state.lock();
486 assert_eq!(state.count, 1, "State: {state:?}");
487 assert_eq!(state.action_triggered_count, 1, "State: {state:?}");
488 }
489
490 #[fuchsia::test]
491 fn test_ignores_presses_while_crash_report_in_progress() {
492 let clock = Arc::new(FakeClock::new());
493 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
494
495 let press_event = make_both_press_event();
496 let release_event = make_release_event();
497
498 for _ in 1..REQUIRED_PRESS_COUNT {
500 assert!(!debug_state.process_button_event(&press_event));
501 assert!(!debug_state.process_button_event(&release_event));
502 }
503 assert!(debug_state.process_button_event(&press_event));
504 {
505 let state = debug_state.button_press_state.lock();
506 assert!(state.crash_report_in_progress);
507 assert_eq!(state.action_triggered_count, 1);
508 }
509
510 assert!(!debug_state.process_button_event(&release_event));
512 assert!(!debug_state.process_button_event(&press_event));
513 assert!(!debug_state.process_button_event(&release_event));
514 {
515 let state = debug_state.button_press_state.lock();
516 assert!(state.crash_report_in_progress);
517 assert_eq!(state.action_triggered_count, 1);
518 assert_eq!(state.count, 0);
519 }
520
521 debug_state.button_press_state.lock().crash_report_in_progress = false;
523
524 assert!(!debug_state.process_button_event(&press_event));
526 let state = debug_state.button_press_state.lock();
527 assert_eq!(state.count, 1);
528 }
529
530 #[fuchsia::test]
531 fn test_counter_resets_after_timeout() {
532 let clock = Arc::new(FakeClock::new());
533 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
534
535 let press_event = make_both_press_event();
536 let release_event = make_release_event();
537
538 for _ in 1..REQUIRED_PRESS_COUNT - 1 {
540 assert!(!debug_state.process_button_event(&press_event));
541 assert!(!debug_state.process_button_event(&release_event));
542 }
543 clock.advance_ns(*MAX_PRESS_INTERVAL_NS + 1);
545
546 assert!(
547 !debug_state.process_button_event(&press_event),
548 "This press should reset the counter to 1, not increment it."
549 );
550 let state = debug_state.button_press_state.lock();
551 assert_eq!(state.count, 1, "State: {state:?}");
552 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
553 }
554
555 #[fuchsia::test]
556 fn test_power_button_press_resets_counter() {
557 let clock = Arc::new(FakeClock::new());
558 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
559
560 let press_event = make_both_press_event();
561 let release_event = make_release_event();
562 let power_press_event =
563 fui_input::MediaButtonsEvent { power: Some(true), ..Default::default() };
564
565 assert!(
566 !debug_state.process_button_event(&press_event),
567 "first volume press should not trigger action"
568 );
569 assert!(!debug_state.process_button_event(&release_event));
570 {
571 let state = debug_state.button_press_state.lock();
572 assert_eq!(state.count, 1, "should have count of 1. State: {state:?}");
573 }
574 assert!(
575 !debug_state.process_button_event(&power_press_event),
576 "power press should not trigger action"
577 );
578 {
579 let state = debug_state.button_press_state.lock();
580 assert_eq!(state.count, 0, "A power press should reset the counter. State: {state:?}");
581 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
582 }
583
584 assert!(
585 !debug_state.process_button_event(&press_event),
586 "The next press should start a new sequence."
587 );
588 let state = debug_state.button_press_state.lock();
589 assert_eq!(state.count, 1, "State: {state:?}");
590 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
591 }
592
593 #[fuchsia::test]
594 fn test_power_button_release_resets_counter() {
595 let clock = Arc::new(FakeClock::new());
596 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
597
598 let vol_press_event = make_both_press_event();
599 let vol_release_event = make_release_event();
600 let power_press_event =
601 fui_input::MediaButtonsEvent { power: Some(true), ..Default::default() };
602 let power_release_event =
603 fui_input::MediaButtonsEvent { power: Some(false), ..Default::default() };
604
605 assert!(
606 !debug_state.process_button_event(&power_press_event),
607 "power press should not trigger action"
608 );
609 {
610 let state = debug_state.button_press_state.lock();
611 assert_eq!(state.count, 0, "count should be 0 after power press. State: {state:?}");
612 }
613 assert!(
614 !debug_state.process_button_event(&vol_press_event),
615 "volume press should not trigger action"
616 );
617 assert!(!debug_state.process_button_event(&vol_release_event));
618 {
619 let state = debug_state.button_press_state.lock();
620 assert_eq!(state.count, 1, "count should be 1 after volume press. State: {state:?}");
621 }
622 assert!(
623 !debug_state.process_button_event(&power_release_event),
624 "A power button release should reset the counter."
625 );
626 {
627 let state = debug_state.button_press_state.lock();
628 assert_eq!(state.count, 0, "count should be 0 after power release. State: {state:?}");
629 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
630 }
631 assert!(
632 !debug_state.process_button_event(&vol_press_event),
633 "The next press should start a new sequence."
634 );
635 let state = debug_state.button_press_state.lock();
636 assert_eq!(state.count, 1, "State: {state:?}");
637 assert_eq!(state.action_triggered_count, 0, "State: {state:?}");
638 }
639
640 #[fuchsia::test]
641 fn test_single_button_press_does_not_increment() {
642 let clock = Arc::new(FakeClock::new());
643 let debug_state = DebugState::new_for_test(true, clock.clone(), None, None);
644
645 let vol_up_event =
646 fui_input::MediaButtonsEvent { volume_up: Some(true), ..Default::default() };
647 let vol_down_event =
648 fui_input::MediaButtonsEvent { volume_down: Some(true), ..Default::default() };
649 let release_event = make_release_event();
650
651 for _ in 0..REQUIRED_PRESS_COUNT {
653 assert!(!debug_state.process_button_event(&vol_up_event));
654 assert!(!debug_state.process_button_event(&release_event));
655 }
656 assert_eq!(debug_state.button_press_state.lock().count, 0);
657
658 for _ in 0..REQUIRED_PRESS_COUNT {
660 assert!(!debug_state.process_button_event(&vol_down_event));
661 assert!(!debug_state.process_button_event(&release_event));
662 }
663 assert_eq!(debug_state.button_press_state.lock().count, 0);
664 }
665
666 async fn run_report_server(mut stream: ffeedback::CrashReporterRequestStream) {
667 let request =
668 stream.try_next().await.expect("failed to read from stream").expect("stream is empty");
669 match request {
670 ffeedback::CrashReporterRequest::FileReport { report, responder } => {
671 assert_eq!(
672 report,
673 ffeedback::CrashReport {
674 program_name: Some("session_manager".to_string()),
675 crash_signature: Some("fuchsia-user-SOS-device-stuck".to_string()),
676 is_fatal: Some(false),
677 ..Default::default()
678 }
679 );
680 responder
681 .send(Ok(&ffeedback::FileReportResults::default()))
682 .expect("failed to send response");
683 }
684 }
685 }
686
687 #[fuchsia::test]
688 async fn test_file_crash_report() {
689 let clock = Arc::new(FakeClock::new());
690 let (crash_reporter_proxy, stream) =
691 create_proxy_and_stream::<ffeedback::CrashReporterMarker>();
692 let (power_statecontrol_admin_proxy, _) = create_proxy_and_stream::<fpower::AdminMarker>();
693
694 let server_fut = run_report_server(stream);
695
696 let debug_state = DebugState::new_for_test(
698 true,
699 clock.clone(),
700 Some(crash_reporter_proxy),
701 Some(power_statecontrol_admin_proxy),
702 );
703 let client_fut = async {
704 assert!(debug_state.file_crash_report().await);
705 };
706
707 join!(client_fut, server_fut);
708 }
709
710 async fn run_reboot_server(mut stream: fpower::AdminRequestStream) {
711 let request =
712 stream.try_next().await.expect("failed to read from stream").expect("stream is empty");
713 match request {
714 fpower::AdminRequest::Shutdown { options, responder } => {
715 assert_eq!(
716 options,
717 fpower::ShutdownOptions {
718 action: Some(fpower::ShutdownAction::Reboot),
719 reasons: Some(vec![fpower::ShutdownReason::UserRequestDeviceStuck]),
720 ..Default::default()
721 }
722 );
723 responder.send(Ok(())).expect("failed to send response");
724 }
725 _ => panic!("unexpected request"),
726 }
727 }
728
729 #[fuchsia::test]
730 async fn test_reboot_device() {
731 let clock = Arc::new(FakeClock::new());
732 let (crash_reporter_proxy, _) = create_proxy_and_stream::<ffeedback::CrashReporterMarker>();
733 let (power_statecontrol_admin_proxy, stream) =
734 create_proxy_and_stream::<fpower::AdminMarker>();
735
736 let server_fut = run_reboot_server(stream);
737
738 let debug_state = DebugState::new_for_test(
740 true,
741 clock.clone(),
742 Some(crash_reporter_proxy),
743 Some(power_statecontrol_admin_proxy),
744 );
745 let reboot_fut = debug_state.reboot_device();
746
747 let (reboot_succeeded, ()) = join!(reboot_fut, server_fut);
749 assert!(reboot_succeeded);
750 }
751}