From 813010dbb6956dbfe085e3ee9975c503d788e329 Mon Sep 17 00:00:00 2001 From: Fimeg Date: Sun, 26 Jul 2026 12:19:52 -0400 Subject: [PATCH] sessiond: proximity stops actuating; it vetoes tap-to-wake and nothing else --- src/sessiond/device_state.rs | 161 +++++++++++++++++------------------ src/sessiond/server.rs | 12 ++- 2 files changed, 88 insertions(+), 85 deletions(-) diff --git a/src/sessiond/device_state.rs b/src/sessiond/device_state.rs index e807faf..ecd71c5 100644 --- a/src/sessiond/device_state.rs +++ b/src/sessiond/device_state.rs @@ -952,20 +952,16 @@ impl SensorEvidence { if self.touch_active { c += 0.1; } - // Cross-sensor disagreement: proximity says near but accelerometer - // says moving — user is walking with phone in hand, not a pocket. - // Don't suppress DPMS wake in this case. + // Proximity near while the device is moving is genuinely ambiguous — + // a phone walking in a pocket and a phone held to an ear read the + // same. It lowers confidence and nothing more; it does not decide the + // wake, which is a question about the session, not the sensors. if self.proximity_near && self.accel_moving { - c -= 0.2; // reduce confidence — ambiguous + c -= 0.2; } c.clamp(0.0, 1.0) } - /// Should DPMS wake be suppressed? (prevent pocket-dial) - pub fn should_suppress_dpms_wake(&self) -> bool { - self.proximity_near && self.confidence() >= 0.3 - } - /// Should idle tier promotion be accelerated? pub fn should_promote_idle_faster(&self) -> bool { self.confidence() >= 0.6 @@ -1067,29 +1063,16 @@ impl DeviceStateMachine { return actions; } - // Pocket or face-down: something is over the sensor while the session - // is locked. Blank now rather than waiting out the idle budget — this - // is the job `blueline-proximity-lock` used to do by calling the DPMS - // executor itself, and it belongs here where it can be weighed - // against the rest of the evidence instead of racing the other rules. + // Proximity does not blank the panel. It used to, on any locked + // screen, which is `blueline-proximity-lock`'s old job moved inward + // and kept too powerful: a covered sensor is a pocket, a face, a + // table, or a thumb, and the machine cannot tell which. Turning the + // screen off on that reading is only right during a call — and the + // machine has no call state yet, so for now it is never right. // - // Note this deliberately fires even when the accelerometer says the - // device is moving (walking with it, or holding it to an ear): a lit - // panel against a face or a pocket lining is not wanted either way. - // The hand-held carve-out in §4 is about not suppressing a *wake*, - // which is a different question from blanking one that is already on. - if is_locked(self.state) && self.sensor_evidence.proximity_near { - info!("[device-state] proximity near while locked — blanking"); - return self.request_blank( - now, - serde_json::json!({ - "reason": "proximity-near", - "confidence": self.sensor_evidence.confidence(), - "accel_moving": self.sensor_evidence.accel_moving, - }), - "locked and something is over the proximity sensor", - ); - } + // What remains of proximity is evidence: it drives `Observed`, it is + // in every snapshot, and it gates tap-to-wake (`suppress_wake`). The + // idle budget below blanks a locked screen soon enough anyway. // The compositor says the user is interacting. Not our business yet — // and crucially this is what keeps a long swipe from being blanked @@ -1444,7 +1427,7 @@ impl DeviceStateMachine { shell_alive, proximity_near: self.sensor_evidence.proximity_near, confidence: self.sensor_evidence.confidence(), - suppress_dpms_wake: self.sensor_evidence.should_suppress_dpms_wake(), + suppress_dpms_wake: self.suppress_wake(InputTrigger::DoubleTapToWake), promote_idle_faster: self.sensor_evidence.should_promote_idle_faster(), screen_locked, screen_lock_secure, @@ -1458,6 +1441,26 @@ impl DeviceStateMachine { self.state } + /// Should this wake be refused because something is over the sensor? + /// + /// Tap-to-wake only. A double tap is the one wake source a pocket can + /// produce by itself, so a covered sensor is the right veto for it. A + /// power button press is intent (§4: "no — hardware signal") and is never + /// refused; neither is a wake the machine itself asked for. + /// + /// This used to be a confidence question — the pair proximity-near + + /// accel-moving read as 0.5 and suppressed everything. Motion cannot tell + /// a pocket from an ear from a hand, so it was never the instrument. + /// + /// **Owed:** a call. Screen dark at the ear is wanted whether or not the + /// session is locked, and it is the only case where proximity should turn + /// a panel *off* rather than decline to turn one on. That needs a + /// call-state input (ModemManager / callaudiod) — a factor, never an + /// authority. + pub fn suppress_wake(&self, trigger: InputTrigger) -> bool { + matches!(trigger, InputTrigger::DoubleTapToWake) && self.sensor_evidence.proximity_near + } + /// Attempt a state transition. Returns true if the transition was /// legal and applied, false if refused (logged as a warning). /// Emits a forensic entry for every attempt (legal or not). @@ -1483,7 +1486,7 @@ impl DeviceStateMachine { shell_alive: false, proximity_near: self.sensor_evidence.proximity_near, confidence: self.sensor_evidence.confidence(), - suppress_dpms_wake: self.sensor_evidence.should_suppress_dpms_wake(), + suppress_dpms_wake: self.suppress_wake(InputTrigger::DoubleTapToWake), promote_idle_faster: self.sensor_evidence.should_promote_idle_faster(), screen_locked: false, screen_lock_secure: false, @@ -1516,7 +1519,7 @@ impl DeviceStateMachine { shell_alive: false, proximity_near: self.sensor_evidence.proximity_near, confidence: self.sensor_evidence.confidence(), - suppress_dpms_wake: self.sensor_evidence.should_suppress_dpms_wake(), + suppress_dpms_wake: self.suppress_wake(InputTrigger::DoubleTapToWake), promote_idle_faster: self.sensor_evidence.should_promote_idle_faster(), screen_locked: false, screen_lock_secure: false, @@ -1639,7 +1642,7 @@ impl DeviceStateMachine { shell_alive: false, proximity_near: self.sensor_evidence.proximity_near, confidence, - suppress_dpms_wake: self.sensor_evidence.should_suppress_dpms_wake(), + suppress_dpms_wake: self.suppress_wake(InputTrigger::DoubleTapToWake), promote_idle_faster: self.sensor_evidence.should_promote_idle_faster(), screen_locked: false, screen_lock_secure: false, @@ -1693,7 +1696,7 @@ impl DeviceStateMachine { shell_alive: false, proximity_near: self.sensor_evidence.proximity_near, confidence: self.sensor_evidence.confidence(), - suppress_dpms_wake: self.sensor_evidence.should_suppress_dpms_wake(), + suppress_dpms_wake: self.suppress_wake(InputTrigger::DoubleTapToWake), promote_idle_faster: self.sensor_evidence.should_promote_idle_faster(), screen_locked: false, screen_lock_secure: false, @@ -1715,7 +1718,7 @@ impl DeviceStateMachine { shell_alive: false, proximity_near: self.sensor_evidence.proximity_near, confidence: self.sensor_evidence.confidence(), - suppress_dpms_wake: self.sensor_evidence.should_suppress_dpms_wake(), + suppress_dpms_wake: self.suppress_wake(InputTrigger::DoubleTapToWake), promote_idle_faster: self.sensor_evidence.should_promote_idle_faster(), screen_locked: false, screen_lock_secure: false, @@ -1744,7 +1747,7 @@ impl DeviceStateMachine { shell_alive: false, proximity_near: self.sensor_evidence.proximity_near, confidence: self.sensor_evidence.confidence(), - suppress_dpms_wake: self.sensor_evidence.should_suppress_dpms_wake(), + suppress_dpms_wake: self.suppress_wake(InputTrigger::DoubleTapToWake), promote_idle_faster: self.sensor_evidence.should_promote_idle_faster(), screen_locked: false, screen_lock_secure: false, @@ -1770,7 +1773,7 @@ impl DeviceStateMachine { "display_active": is_display_active(self.state), "observed": matches!(self.state, DeviceState::Observed), "observed_confidence": self.sensor_evidence.confidence(), - "suppress_dpms_wake": self.sensor_evidence.should_suppress_dpms_wake(), + "suppress_dpms_wake": self.suppress_wake(InputTrigger::DoubleTapToWake), "panel_on": self.panel_on, "user_active": self.user_active(), "idle_secs": self.idle_secs(), @@ -1881,22 +1884,6 @@ mod tests { assert!(!is_locked(sm.state)); } - #[test] - fn proximity_cannot_blank_after_a_logind_unlock() { - // The symptom Casey reported: proximity blanking an unlocked phone. - // The proximity rule was always correctly gated on is_locked — the - // state was the thing lying. - let (mut sm, t0) = locked_and_lit(); - sm.set_session_locked(false); - - sm.sensor_evidence.proximity_near = true; - sm.mark_evidence_seen(SensorSource::Proximity); - assert!( - sm.tick_at(t0 + Duration::from_secs(1)).is_empty(), - "a hand over the sensor must not blank an unlocked phone" - ); - } - #[test] fn unlocking_cancels_a_pending_blank_and_restores_the_dim() { let (mut sm, t0) = locked_and_lit(); @@ -2069,26 +2056,22 @@ mod tests { } #[test] - fn proximity_near_while_locked_blanks_at_once() { - let (mut sm, t0) = locked_and_lit(); - sm.sensor_evidence.proximity_near = true; - sm.mark_evidence_seen(SensorSource::Proximity); - // No waiting for the idle budget: it is in a pocket now. - assert_eq!( - sm.tick_at(t0 + Duration::from_millis(10)), - vec![Action::Blank] - ); - } - - #[test] - fn proximity_near_while_unlocked_is_left_alone() { - let mut sm = DeviceStateMachine::new(); - let t0 = Instant::now(); - sm.sensor_evidence.proximity_near = true; - sm.mark_evidence_seen(SensorSource::Proximity); - // A hand over the sensor on an unlocked, in-use phone is not a - // reason to blank it. - assert!(sm.tick_at(t0 + Duration::from_secs(1)).is_empty()); + fn proximity_alone_never_blanks_the_panel() { + // A covered sensor is a pocket, a face, a table or a thumb, and the + // machine cannot tell which. The only reading that justifies turning + // the screen off is a call, and the machine has no call state yet. + for locked in [true, false] { + let (mut sm, t0) = locked_and_lit(); + if !locked { + sm.set_session_locked(false); + } + sm.sensor_evidence.proximity_near = true; + sm.mark_evidence_seen(SensorSource::Proximity); + assert!( + sm.tick_at(t0 + Duration::from_millis(10)).is_empty(), + "locked={locked}: proximity is evidence, not an actuator" + ); + } } #[test] @@ -2147,11 +2130,11 @@ mod tests { let (mut sm, t0) = locked_and_lit(); sm.sensor_evidence.proximity_near = true; sm.mark_evidence_seen(SensorSource::Proximity); - assert!(sm.sensor_evidence.should_suppress_dpms_wake()); + assert!(sm.suppress_wake(InputTrigger::DoubleTapToWake)); // A dead proximity sensor must not keep vetoing wakes forever. sm.tick_at(t0 + EVIDENCE_TTL + Duration::from_secs(1)); - assert!(!sm.sensor_evidence.should_suppress_dpms_wake()); + assert!(!sm.suppress_wake(InputTrigger::DoubleTapToWake)); } #[test] @@ -2260,7 +2243,8 @@ mod tests { #[test] fn sensor_disagreement_reduces_confidence() { - // proximity near + accel moving = walking with phone, not pocket + // Proximity near + accel moving: a pocket, an ear and a hand all read + // this way. It lowers confidence and decides nothing. let e = SensorEvidence { proximity_near: true, accel_moving: true, @@ -2269,12 +2253,27 @@ mod tests { }; // 0.4 + 0.3 - 0.2 = 0.5 assert!((e.confidence() - 0.5).abs() < 0.01); - // DPMS wake suppression still on (0.5 >= 0.3) - assert!(e.should_suppress_dpms_wake()); - // But idle promotion not accelerated (0.5 < 0.6) + // Idle promotion not accelerated (0.5 < 0.6). assert!(!e.should_promote_idle_faster()); } + #[test] + fn only_tap_to_wake_is_vetoed_by_a_covered_sensor() { + // A double tap is the one wake a pocket can produce by itself. The + // power button is intent and is never refused, whatever the sensors + // say, locked or not. + let mut sm = DeviceStateMachine::new(); + sm.sensor_evidence.proximity_near = true; + sm.sensor_evidence.accel_moving = true; + + for state in [DeviceState::Active, DeviceState::Locked] { + sm.transition(state); + assert!(sm.suppress_wake(InputTrigger::DoubleTapToWake)); + assert!(!sm.suppress_wake(InputTrigger::PowerButton)); + assert!(!sm.suppress_wake(InputTrigger::Touch)); + } + } + #[test] fn proximity_triggers_observed() { let mut sm = DeviceStateMachine::new(); diff --git a/src/sessiond/server.rs b/src/sessiond/server.rs index b1d21e8..5d6cfbe 100644 --- a/src/sessiond/server.rs +++ b/src/sessiond/server.rs @@ -605,7 +605,6 @@ fn handle_request( } } let conf = evidence.confidence(); - let suppress = evidence.should_suppress_dpms_wake(); let promote = evidence.should_promote_idle_faster(); // Copy the readings out, ending the borrow before the machine is // touched again. @@ -619,13 +618,18 @@ fn handle_request( // on the next tick, because "never reported" and "reported long // ago" are the same thing to the staleness rule. d.device_state.mark_evidence_seen(input.source); + // Let the state machine evaluate Observed transitions. + d.device_state + .update_sensors_from(input.source, evidence_copy); + // The tap-to-wake answer specifically; a power button is never + // refused. Asked after the update, not before. + let suppress = d + .device_state + .suppress_wake(InputTrigger::DoubleTapToWake); info!( "[device-state] sensor {:?} = {:?} (confidence={:.2}, suppress_dpms={}, promote_idle={})", input.source, input.value, conf, suppress, promote ); - // Let the state machine evaluate Observed transitions. - d.device_state - .update_sensors_from(input.source, evidence_copy); serde_json::json!({ "ok": true, "confidence": conf,