diff --git a/src/main/java/wtf/beatrice/autosqueal/listener/CursorMoveListener.java b/src/main/java/wtf/beatrice/autosqueal/listener/CursorMoveListener.java index 04a06ec..49dfed5 100644 --- a/src/main/java/wtf/beatrice/autosqueal/listener/CursorMoveListener.java +++ b/src/main/java/wtf/beatrice/autosqueal/listener/CursorMoveListener.java @@ -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) { diff --git a/src/main/java/wtf/beatrice/autosqueal/listener/KeyPressListener.java b/src/main/java/wtf/beatrice/autosqueal/listener/KeyPressListener.java index 3d28cbb..8d1353e 100644 --- a/src/main/java/wtf/beatrice/autosqueal/listener/KeyPressListener.java +++ b/src/main/java/wtf/beatrice/autosqueal/listener/KeyPressListener.java @@ -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()); } diff --git a/src/main/java/wtf/beatrice/autosqueal/ui/MainWindow.java b/src/main/java/wtf/beatrice/autosqueal/ui/MainWindow.java index 5191d72..ee35802 100644 --- a/src/main/java/wtf/beatrice/autosqueal/ui/MainWindow.java +++ b/src/main/java/wtf/beatrice/autosqueal/ui/MainWindow.java @@ -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, diff --git a/src/test/java/wtf/beatrice/autosqueal/listener/CursorMoveListenerTest.java b/src/test/java/wtf/beatrice/autosqueal/listener/CursorMoveListenerTest.java index 405093c..3533e8d 100644 --- a/src/test/java/wtf/beatrice/autosqueal/listener/CursorMoveListenerTest.java +++ b/src/test/java/wtf/beatrice/autosqueal/listener/CursorMoveListenerTest.java @@ -130,6 +130,53 @@ class CursorMoveListenerTest assertTrue(listener.isUserAway()); } + @Test + void keyboardActivityResetsTheAwayTimer() { + List 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