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
|
* 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
|
* 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 {
|
public class CursorMoveListener implements Runnable {
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ public class CursorMoveListener implements Runnable {
|
|||||||
private boolean firstPoll = true;
|
private boolean firstPoll = true;
|
||||||
private int loops = 0;
|
private int loops = 0;
|
||||||
private volatile boolean userAway = false;
|
private volatile boolean userAway = false;
|
||||||
|
private volatile boolean keyboardActivity = false;
|
||||||
|
|
||||||
public CursorMoveListener(RobotMouseTracker robotTracker) {
|
public CursorMoveListener(RobotMouseTracker robotTracker) {
|
||||||
this(robotTracker, () -> MouseInfo.getPointerInfo().getLocation());
|
this(robotTracker, () -> MouseInfo.getPointerInfo().getLocation());
|
||||||
@@ -37,9 +39,22 @@ public class CursorMoveListener implements Runnable {
|
|||||||
this.currentPosition = currentPosition;
|
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
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
|
// consume the keyboard activity reported since the previous poll
|
||||||
|
boolean keyboardActivity = this.keyboardActivity;
|
||||||
|
this.keyboardActivity = false;
|
||||||
|
|
||||||
Point location = currentPosition.get();
|
Point location = currentPosition.get();
|
||||||
|
|
||||||
if (firstPoll) {
|
if (firstPoll) {
|
||||||
@@ -52,7 +67,7 @@ public class CursorMoveListener implements Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean positionChanged = location.x != lastSeenX || location.y != lastSeenY;
|
boolean positionChanged = location.x != lastSeenX || location.y != lastSeenY;
|
||||||
boolean userMoved = positionChanged && !isAppMovement(location);
|
boolean userMoved = keyboardActivity || (positionChanged && !isAppMovement(location));
|
||||||
|
|
||||||
if (userMoved) {
|
if (userMoved) {
|
||||||
if (userAway) {
|
if (userAway) {
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ public class KeyPressListener implements NativeKeyListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
pressedKeys.add(e.getKeyCode());
|
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) {
|
// any key the user presses means they are present: let the away
|
||||||
Main.unregisterJNativeHook();
|
// 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
|
// 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
|
// 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
|
@Override
|
||||||
public void nativeKeyReleased(NativeKeyEvent e) {
|
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());
|
pressedKeys.remove((Integer) e.getKeyCode());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ public class MainWindow
|
|||||||
private final JFrame frame = new JFrame();
|
private final JFrame frame = new JFrame();
|
||||||
private JButton toggleButton;
|
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 ScheduledExecutorService periodicScheduler;
|
||||||
private ExecutorService movementExecutor;
|
private ExecutorService movementExecutor;
|
||||||
private CursorMover cursorMover;
|
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
|
* 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.
|
* 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
|
// away-detection: polls the cursor position and ignores the movements
|
||||||
// the app performs itself, as reported by the robot mouse tracker.
|
// the app performs itself, as reported by the robot mouse tracker.
|
||||||
// the automation only moves the mouse when the user is away.
|
// the automation only moves the mouse when the user is away.
|
||||||
RobotMouseTracker robotTracker = new RobotMouseTracker();
|
periodicScheduler.scheduleWithFixedDelay(awayDetector, 0L, AWAY_POLL_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
||||||
CursorMoveListener cursorMoveListener = new CursorMoveListener(robotTracker);
|
|
||||||
periodicScheduler.scheduleWithFixedDelay(cursorMoveListener, 0L, AWAY_POLL_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
cursorMover = new CursorMover(movementExecutor, robotTracker, cursorMoveListener::isUserAway);
|
cursorMover = new CursorMover(movementExecutor, robotTracker, awayDetector::isUserAway);
|
||||||
periodicScheduler.scheduleWithFixedDelay(cursorMover,
|
periodicScheduler.scheduleWithFixedDelay(cursorMover,
|
||||||
AUTOMATION_START_DELAY_SECONDS,
|
AUTOMATION_START_DELAY_SECONDS,
|
||||||
RunnerUtil.SECONDS_BETWEEN_MOVES,
|
RunnerUtil.SECONDS_BETWEEN_MOVES,
|
||||||
|
|||||||
@@ -130,6 +130,53 @@ class CursorMoveListenerTest
|
|||||||
assertTrue(listener.isUserAway());
|
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
|
@Test
|
||||||
void userComingBackIsDetectedWhileTheAppIsWiggling() {
|
void userComingBackIsDetectedWhileTheAppIsWiggling() {
|
||||||
// the app keeps wiggling while the user is away; when the user comes
|
// the app keeps wiggling while the user is away; when the user comes
|
||||||
|
|||||||
Reference in New Issue
Block a user