sessiond: squeeze is input, and a pocket can veto it
Active Edge becomes InputTrigger::Squeeze. It resets the idle budget and lands in the trail like any real input, but unlike a power button it is a sensor reading and a tight pocket is a squeezed chassis, so it carries the same proximity veto as tap-to-wake (DEVICE-STATE-MACHINE 4). note_input_gated applies the veto and records the refusal via record_decision — a silently dropped report is what 10 exists to prevent. The input op now advertises refused_by_state. Producers cooperate rather than being enforced; TASK-41 is the gate.
This commit is contained in:
parent
68b6025226
commit
c48737620e
3 changed files with 92 additions and 4 deletions
|
|
@ -518,6 +518,7 @@ pub enum ForensicEvent {
|
||||||
pub enum WakeTrigger {
|
pub enum WakeTrigger {
|
||||||
PowerButton,
|
PowerButton,
|
||||||
DoubleTapToWake,
|
DoubleTapToWake,
|
||||||
|
Squeeze,
|
||||||
ProximityFar,
|
ProximityFar,
|
||||||
RtcAlarm,
|
RtcAlarm,
|
||||||
ModemIrq,
|
ModemIrq,
|
||||||
|
|
@ -1577,6 +1578,39 @@ impl DeviceStateMachine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Real user input, with the machine's own veto applied first.
|
||||||
|
///
|
||||||
|
/// `note_input` is unconditional by design: a power button is intent and is
|
||||||
|
/// never refused (§4, "no — hardware signal"). A squeeze is not a button.
|
||||||
|
/// It is a strain reading, and a phone in a tight pocket is a squeezed
|
||||||
|
/// chassis — the same failure a covered proximity sensor already vetoes for
|
||||||
|
/// tap-to-wake. Routing squeeze through here rather than straight to
|
||||||
|
/// `note_input` is what stops a pocket from resetting the idle budget and
|
||||||
|
/// opening a verb surface.
|
||||||
|
///
|
||||||
|
/// The refusal is recorded, not dropped. §10 spent a section establishing
|
||||||
|
/// that absence must never be mistaken for a negative; a producer whose
|
||||||
|
/// reports vanish silently is indistinguishable from a dead one.
|
||||||
|
pub fn note_input_gated(&mut self, trigger: InputTrigger) -> Option<Vec<Action>> {
|
||||||
|
if self.suppress_wake(trigger) {
|
||||||
|
warn!(
|
||||||
|
"[device-state] {:?} refused — proximity near, treating as pocket",
|
||||||
|
trigger
|
||||||
|
);
|
||||||
|
self.record_decision(
|
||||||
|
"input-refused",
|
||||||
|
serde_json::json!({
|
||||||
|
"trigger": format!("{:?}", trigger),
|
||||||
|
"proximity_near": self.sensor_evidence.proximity_near,
|
||||||
|
"confidence": self.sensor_evidence.confidence(),
|
||||||
|
}),
|
||||||
|
"a covered sensor is the pocket veto (DEVICE-STATE-MACHINE §4)",
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(self.note_input(trigger))
|
||||||
|
}
|
||||||
|
|
||||||
/// Real user input. Resets the blank budget, re-arms the rule, and undoes
|
/// Real user input. Resets the blank budget, re-arms the rule, and undoes
|
||||||
/// the pre-warning dim if one is showing — that cancel is the whole point
|
/// the pre-warning dim if one is showing — that cancel is the whole point
|
||||||
/// of the grace window, so it returns the action rather than waiting for
|
/// of the grace window, so it returns the action rather than waiting for
|
||||||
|
|
@ -1599,6 +1633,7 @@ impl DeviceStateMachine {
|
||||||
let wake = match trigger {
|
let wake = match trigger {
|
||||||
InputTrigger::PowerButton => WakeTrigger::PowerButton,
|
InputTrigger::PowerButton => WakeTrigger::PowerButton,
|
||||||
InputTrigger::DoubleTapToWake => WakeTrigger::DoubleTapToWake,
|
InputTrigger::DoubleTapToWake => WakeTrigger::DoubleTapToWake,
|
||||||
|
InputTrigger::Squeeze => WakeTrigger::Squeeze,
|
||||||
InputTrigger::Touch | InputTrigger::Key => WakeTrigger::UserInput,
|
InputTrigger::Touch | InputTrigger::Key => WakeTrigger::UserInput,
|
||||||
InputTrigger::Unknown => WakeTrigger::Unknown,
|
InputTrigger::Unknown => WakeTrigger::Unknown,
|
||||||
};
|
};
|
||||||
|
|
@ -1718,7 +1753,10 @@ impl DeviceStateMachine {
|
||||||
/// call-state input (ModemManager / callaudiod) — a factor, never an
|
/// call-state input (ModemManager / callaudiod) — a factor, never an
|
||||||
/// authority.
|
/// authority.
|
||||||
pub fn suppress_wake(&self, trigger: InputTrigger) -> bool {
|
pub fn suppress_wake(&self, trigger: InputTrigger) -> bool {
|
||||||
matches!(trigger, InputTrigger::DoubleTapToWake) && self.sensor_evidence.proximity_near
|
matches!(
|
||||||
|
trigger,
|
||||||
|
InputTrigger::DoubleTapToWake | InputTrigger::Squeeze
|
||||||
|
) && self.sensor_evidence.proximity_near
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt a state transition. Returns true if the transition was
|
/// Attempt a state transition. Returns true if the transition was
|
||||||
|
|
@ -2487,6 +2525,33 @@ mod tests {
|
||||||
assert_eq!(actions, vec![Action::Blank]);
|
assert_eq!(actions, vec![Action::Blank]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn squeeze_is_vetoed_by_proximity_but_a_power_button_is_not() {
|
||||||
|
let (mut sm, _t0) = locked_and_lit();
|
||||||
|
sm.sensor_evidence.proximity_near = true;
|
||||||
|
sm.mark_evidence_seen(SensorSource::Proximity);
|
||||||
|
|
||||||
|
// A squeezed chassis in a pocket reads exactly like a deliberate
|
||||||
|
// squeeze. A covered sensor is the veto, same as tap-to-wake.
|
||||||
|
assert!(sm.suppress_wake(InputTrigger::Squeeze));
|
||||||
|
assert!(sm.note_input_gated(InputTrigger::Squeeze).is_none());
|
||||||
|
|
||||||
|
// Intent from a hardware button is never refused (§4).
|
||||||
|
assert!(!sm.suppress_wake(InputTrigger::PowerButton));
|
||||||
|
assert!(sm.note_input_gated(InputTrigger::PowerButton).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_uncovered_squeeze_is_real_input() {
|
||||||
|
let (mut sm, _t0) = locked_and_lit();
|
||||||
|
sm.sensor_evidence.proximity_near = false;
|
||||||
|
|
||||||
|
assert!(!sm.suppress_wake(InputTrigger::Squeeze));
|
||||||
|
assert!(sm.note_input_gated(InputTrigger::Squeeze).is_some());
|
||||||
|
// It resets the idle budget like any other real input.
|
||||||
|
assert!(sm.idle_since.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stale_proximity_stops_suppressing_wake() {
|
fn stale_proximity_stops_suppressing_wake() {
|
||||||
let (mut sm, t0) = locked_and_lit();
|
let (mut sm, t0) = locked_and_lit();
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,9 @@ pub const VERBS: &[VerbDoc] = &[
|
||||||
op: "input",
|
op: "input",
|
||||||
mutates: true,
|
mutates: true,
|
||||||
summary: "real user input happened; resets the idle budget",
|
summary: "real user input happened; resets the idle budget",
|
||||||
refuses: &[],
|
// A squeeze is refusable — a pocket can produce one, and proximity is
|
||||||
|
// the veto (DEVICE-STATE-MACHINE §4). A power button never is.
|
||||||
|
refuses: &[RefusalCode::RefusedByState],
|
||||||
example: r#"{"op":"input","trigger":"touch"}"#,
|
example: r#"{"op":"input","trigger":"touch"}"#,
|
||||||
},
|
},
|
||||||
VerbDoc {
|
VerbDoc {
|
||||||
|
|
@ -289,6 +291,13 @@ pub enum InputTrigger {
|
||||||
Key,
|
Key,
|
||||||
PowerButton,
|
PowerButton,
|
||||||
DoubleTapToWake,
|
DoubleTapToWake,
|
||||||
|
/// Active Edge — the frame was squeezed. Deliberate intent, like a button,
|
||||||
|
/// but unlike a button it is a sensor and it can be produced by a pocket:
|
||||||
|
/// a squeezed chassis is exactly what a phone in a tight pocket is. So it
|
||||||
|
/// carries the same proximity veto as tap-to-wake (`suppress_wake`), and
|
||||||
|
/// for the same reason — see DEVICE-STATE-MACHINE §4, "a covered sensor is
|
||||||
|
/// the right veto for the one wake a pocket can produce by itself".
|
||||||
|
Squeeze,
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -806,9 +806,23 @@ fn handle_request(
|
||||||
}
|
}
|
||||||
Request::Input { trigger } => {
|
Request::Input { trigger } => {
|
||||||
let trigger = trigger.unwrap_or(InputTrigger::Unknown);
|
let trigger = trigger.unwrap_or(InputTrigger::Unknown);
|
||||||
let actions = {
|
let gated = {
|
||||||
let mut d = shared.lock();
|
let mut d = shared.lock();
|
||||||
d.device_state.note_input(trigger)
|
d.device_state.note_input_gated(trigger)
|
||||||
|
};
|
||||||
|
// A refusal, in the vocabulary callers already branch on: a gesture
|
||||||
|
// producer must not act on an input the authority refused, and
|
||||||
|
// `refused_by_state` says not-now rather than never (doctrine §13).
|
||||||
|
//
|
||||||
|
// Today this is cooperation, not enforcement — an unattested
|
||||||
|
// producer can ignore the answer and call the shell anyway. §10 is
|
||||||
|
// explicit that the policy layer means something before the
|
||||||
|
// enforcement lands; TASK-41 is the enforcement.
|
||||||
|
let Some(actions) = gated else {
|
||||||
|
return refuse(
|
||||||
|
RefusalCode::RefusedByState,
|
||||||
|
"proximity near — treated as a pocket",
|
||||||
|
);
|
||||||
};
|
};
|
||||||
let cancelled = !actions.is_empty();
|
let cancelled = !actions.is_empty();
|
||||||
for action in actions {
|
for action in actions {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue