feed keyboard activity into the away detection

This commit is contained in:
bea
2026-10-01 22:48:15 +02:00
parent 10c31b6834
commit 4a7b4267f5
4 changed files with 84 additions and 11 deletions
@@ -10,7 +10,8 @@ import java.util.function.Supplier;
/**
* Watches the cursor position and tracks whether the user is away: if the cursor
* stays still for long enough — ignoring the movements the app performs on its
* own — the user is considered away.
* own — the user is considered away. Keyboard activity counts as presence,
* too: a user who is typing is using the machine, even if the mouse never moves.
*/
public class CursorMoveListener implements Runnable {
@@ -27,6 +28,7 @@ public class CursorMoveListener implements Runnable {
private boolean firstPoll = true;
private int loops = 0;
private volatile boolean userAway = false;
private volatile boolean keyboardActivity = false;
public CursorMoveListener(RobotMouseTracker robotTracker) {
this(robotTracker, () -> MouseInfo.getPointerInfo().getLocation());
@@ -37,9 +39,22 @@ public class CursorMoveListener implements Runnable {
this.currentPosition = currentPosition;
}
/**
* Reports that the user pressed a key: keyboard activity counts as
* presence, so it resets the away timer at the next poll. Safe to call
* from any thread.
*/
public void reportKeyboardActivity() {
keyboardActivity = true;
}
@Override
public void run() {
// consume the keyboard activity reported since the previous poll
boolean keyboardActivity = this.keyboardActivity;
this.keyboardActivity = false;
Point location = currentPosition.get();
if (firstPoll) {
@@ -52,7 +67,7 @@ public class CursorMoveListener implements Runnable {
}
boolean positionChanged = location.x != lastSeenX || location.y != lastSeenY;
boolean userMoved = positionChanged && !isAppMovement(location);
boolean userMoved = keyboardActivity || (positionChanged && !isAppMovement(location));
if (userMoved) {
if (userAway) {
@@ -23,11 +23,11 @@ public class KeyPressListener implements NativeKeyListener
}
pressedKeys.add(e.getKeyCode());
LOGGER.info("Key Pressed: {}", NativeKeyEvent.getKeyText(e.getKeyCode()));
LOGGER.debug("Key Pressed: {}", NativeKeyEvent.getKeyText(e.getKeyCode()));
if (e.getKeyCode() == NativeKeyEvent.VC_ESCAPE) {
Main.unregisterJNativeHook();
}
// any key the user presses means they are present: let the away
// detection know, so the app doesn't grab the mouse while they type
Main.getMainWindow().notifyKeyboardActivity();
// toggle only when the second key of the combo is pressed down: not only does this
// fire exactly once per combo, but it also avoids toggling when any other key is
@@ -44,7 +44,7 @@ public class KeyPressListener implements NativeKeyListener
@Override
public void nativeKeyReleased(NativeKeyEvent e) {
LOGGER.info("Key Released: {}", NativeKeyEvent.getKeyText(e.getKeyCode()));
LOGGER.debug("Key Released: {}", NativeKeyEvent.getKeyText(e.getKeyCode()));
pressedKeys.remove((Integer) e.getKeyCode());
}
@@ -34,6 +34,11 @@ public class MainWindow
private final JFrame frame = new JFrame();
private JButton toggleButton;
// the away detection tracks the user, not the automation: it lives from
// the window's creation and is polled only while the automation runs
private final RobotMouseTracker robotTracker = new RobotMouseTracker();
private final CursorMoveListener awayDetector = new CursorMoveListener(robotTracker);
private ScheduledExecutorService periodicScheduler;
private ExecutorService movementExecutor;
private CursorMover cursorMover;
@@ -235,6 +240,14 @@ public class MainWindow
}
}
/**
* Forwards keyboard activity to the away detection, so that typing
* counts as presence even when the mouse never moves.
*/
public void notifyKeyboardActivity() {
awayDetector.reportKeyboardActivity();
}
/**
* Toggles the automation on or off. Safe to call from any thread: the actual
* work is always marshalled to the EDT, since it touches Swing components.
@@ -269,11 +282,9 @@ public class MainWindow
// away-detection: polls the cursor position and ignores the movements
// the app performs itself, as reported by the robot mouse tracker.
// the automation only moves the mouse when the user is away.
RobotMouseTracker robotTracker = new RobotMouseTracker();
CursorMoveListener cursorMoveListener = new CursorMoveListener(robotTracker);
periodicScheduler.scheduleWithFixedDelay(cursorMoveListener, 0L, AWAY_POLL_INTERVAL_SECONDS, TimeUnit.SECONDS);
periodicScheduler.scheduleWithFixedDelay(awayDetector, 0L, AWAY_POLL_INTERVAL_SECONDS, TimeUnit.SECONDS);
cursorMover = new CursorMover(movementExecutor, robotTracker, cursorMoveListener::isUserAway);
cursorMover = new CursorMover(movementExecutor, robotTracker, awayDetector::isUserAway);
periodicScheduler.scheduleWithFixedDelay(cursorMover,
AUTOMATION_START_DELAY_SECONDS,
RunnerUtil.SECONDS_BETWEEN_MOVES,
@@ -130,6 +130,53 @@ class CursorMoveListenerTest
assertTrue(listener.isUserAway());
}
@Test
void keyboardActivityResetsTheAwayTimer() {
List<Point> positions = new ArrayList<>();
positions.add(at(100, 100)); // baseline
for (int i = 0; i < 29; i++) positions.add(at(100, 100)); // 29 still polls
positions.add(at(100, 100)); // the user presses a key
for (int i = 0; i < LOOPS_BEFORE_AWAY; i++) positions.add(at(100, 100)); // then the mouse stays put
CursorMoveListener listener = scriptedListener(positions.toArray(Point[]::new));
listener.run(); // seed the baseline
for (int i = 0; i < 29; i++) listener.run(); // loops = 29
assertFalse(listener.isUserAway());
// the user is typing without moving the mouse: the timer resets
listener.reportKeyboardActivity();
listener.run();
assertFalse(listener.isUserAway());
// ...and it still takes 30 still polls to be away again
for (int i = 0; i < LOOPS_BEFORE_AWAY - 1; i++) listener.run(); // loops = 29
assertFalse(listener.isUserAway());
listener.run(); // loops = 30
assertTrue(listener.isUserAway());
}
@Test
void keyboardActivityBringsTheUserBackWhileAway() {
Point[] positions = new Point[LOOPS_BEFORE_AWAY + 3];
for (int i = 0; i < positions.length; i++) {
positions[i] = at(100, 100);
}
CursorMoveListener listener = scriptedListener(positions);
for (int i = 0; i <= LOOPS_BEFORE_AWAY; i++) { // baseline + 30 still polls
listener.run();
}
assertTrue(listener.isUserAway());
// the user comes back at the keyboard: the away state must end,
// even though the mouse never moved
listener.reportKeyboardActivity();
listener.run();
assertFalse(listener.isUserAway());
}
@Test
void userComingBackIsDetectedWhileTheAppIsWiggling() {
// the app keeps wiggling while the user is away; when the user comes