Watch
1
0
Fork
You've already forked souveraine
0

sessiond: proximity stops actuating; it vetoes tap-to-wake and nothing else

This commit is contained in:
Fimeg 2026-07-26 12:19:52 -04:00
commit 813010dbb6
2 changed files with 88 additions and 85 deletions

View file

@ -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();

View file

@ -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,