diff --git a/src/sessiond/device_state.rs b/src/sessiond/device_state.rs index 48bcaca..a89fe8b 100644 --- a/src/sessiond/device_state.rs +++ b/src/sessiond/device_state.rs @@ -1397,7 +1397,7 @@ pub struct DeviceStateMachine { /// makes this the sixth edge-versus-level bug here, after `locked_ack`, /// `ChargeRate`, `bootBloomActive`, `hasLoginctl` and the dormant OR in the /// compositor's `disclosure_locked`. - press_began_dark: bool, + press_began_dark: std::collections::HashMap, /// True once a blank has been asked for, so we ask exactly once per wake /// instead of every tick. blank_requested: bool, @@ -1515,7 +1515,7 @@ impl DeviceStateMachine { observed_since: None, // Boot comes up lit, so no press is in flight and the latch would // only ever be read after a real DOWN edge has set it. - press_began_dark: false, + press_began_dark: std::collections::HashMap::new(), dimmed: false, pending_blank: None, brightness_before_dim: None, @@ -1581,7 +1581,20 @@ impl DeviceStateMachine { // is what lights the panel, so this is the last moment the answer is // still about the screen the user actually pressed against. if edge == ButtonEdge::Down { - self.press_began_dark = !self.panel_on; + // Per button, not one field for all of them. + // + // A tap never resolves on the UP edge — `ButtonRecognizer::up` + // returns `None` and the tap lands when the 300 ms multi-tap window + // closes on a later tick, up to about 1.3 s after the press. A + // single shared latch had to survive that whole gap, and any other + // button's DOWN edge inside it overwrote the answer: power wakes a + // dark phone, the volume rocker beside it gets nudged within the + // second, and the pending power tap resolves as "the panel was lit" + // and blanks the screen. That is the exact regression the latch was + // added to fix, coming back through a second button. + // + // `buttons` is keyed per button for this reason already. + self.press_began_dark.insert(button, !self.panel_on); } let rec = self.buttons.entry(button).or_default(); let gesture = match edge { @@ -1658,9 +1671,9 @@ impl DeviceStateMachine { if button != Button::Power || gesture != ButtonGesture::Tap { return Vec::new(); } - // The panel as it was when the press began, not as it is now. See - // `press_began_dark`: this same press already woke it. - if self.press_began_dark { + // The panel as it was when THIS button's press began, not as it is + // now. See `press_began_dark`: this same press already woke it. + if self.press_began_dark.remove(&button).unwrap_or(!self.panel_on) { // Dark: the tap is a wake, and a power-button wake is never vetoed. // // `Unblank` *then* `Restore`, and the order is the whole of it: the @@ -3260,6 +3273,29 @@ mod tests { ); } + #[test] + fn a_volume_nudge_does_not_make_the_power_tap_blank_the_screen() { + // The latch used to be one field for every button. A tap resolves on + // the multi-tap window closing, up to ~1.3 s after the press, so any + // other button's DOWN edge inside that gap overwrote it — and on a + // Pixel 3 the volume rocker is under the same grip as power. + let (mut sm, t0) = unlocked_and_lit(); + sm.set_panel(false); + sm.button_edge_at(Button::Power, ButtonEdge::Down, t0); + sm.set_panel(true); // the wake this press caused + sm.button_edge_at(Button::Power, ButtonEdge::Up, t0 + Duration::from_millis(80)); + + // The rocker, well inside the multi-tap window. + sm.button_edge_at(Button::VolumeUp, ButtonEdge::Down, t0 + Duration::from_millis(120)); + sm.button_edge_at(Button::VolumeUp, ButtonEdge::Up, t0 + Duration::from_millis(200)); + + let actions = sm.tick_at(t0 + Duration::from_secs(2)); + assert!( + !actions.iter().any(|a| matches!(a, Action::Blank | Action::Lock)), + "the volume press must not decide what the power press meant: {actions:?}" + ); + } + #[test] fn a_press_against_a_lit_panel_still_blanks_it() { // The other half — without this the fix would simply disable the power