feed keyboard activity into the away detection
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user