When I decided to add automatic idle and away-status to Sim, my small instant messenger for local networks, I began searching furiously at any means to implement this feature in pure Java, but I came up with nothing :

  • Robot lets you position the pointer at whichever location you want but does not let you poll the position.

    [Edit 2008-02-15] Of course, as mentioned in the comments, since Java 1.5 you can use the PointerInfo class to poll the mouse position on any platform… It still does not reveal keyboard activity (annoying for hard geeks that might not even touch their mouse once a day), but it’s the best fallback method you can think of. Still a bit ashamed of not discovering this useful class earlier… I should have RTFM 😀

  • you can listen for mouse and keyboard events, but only from a frame that has to be focused (for keyboard events) and / or that fills the whole screen (for mouse events). And the Mac OS X implementation of Java does not even seem to trigger any mouse motion event to a non-focused window.

In the native world, though, there are plenty of ways to get the job done :

  • On “modern” Windows versions (Windows 2000 and more recent), there is the GetLastInputInfo method in user32.dll that just takes care of the whole thing (gives time of the last mouse or keyboard activity)
  • On older Windows versions, there are some obscure ways to register event hooks in the system which I haven’t investigated much but which have lots of associated online howtos
  • In X11, it is easy to query the mouse position (using XQueryPointer on an artificial empty window)

    [Edit 2008-02-15] As someone asked me how to do this, here is the source code of the tiny C program I use from Sim : getx11mouse.c{#p117}. It should be easy to translate it to Java + JNA, but again, the PointerInfo makes it useless to do so if you use Java 1.5+.

  • On Mac OS X, there is a GetMouse function in the Carbon Event Manager, and I have no doubt there are some nice tricks with Cocoa, but I couldn’t find anything about the keyboard.

So how do we do that in Java ?

Well, I can see 4 ways to do it :

  1. JNI : yuck ! Unless someone comes up with an easy way to generate the glue code, cross-compile and distribute all the resulting executables, there is no way I ever use it for general-audience projects !
  2. Ship small native executables that output the mouse and keyboard activity info in your JAR, unpack them to some temporary location and call them through Runtime.getRuntime().exec(String). This is heavy and dirty, but it kinda works… I actually used this method first for Sim. The biggest issue is that you have to maintain different executables for each target platform.
  3. Ship the source for the small executables mentioned above and hope that the system has a compiler, so as to be able to compile them at the last minute. This is SOOOO dirty and soooo relevant of a desperate programming situation, but believe it or not I actually did that for Sim on Solaris and Linux systems, and it works pretty well (most if not all linux setups ship with gcc).
  4. Call native APIs directly from Java, with no JNI code to write at all

Wait a minute… did I just read “native” and “no JNI” in the same sentence ?

Yep ! Enter the magic world of JNA, the Java Native Access library.

JNA is a great library that lets you call native functions from dynamic libraries (.dll files in Windows, .dylib on Mac OS X, .so on other Unices), only requiring from you to declare the headers of these functions (and of their argument’s structures, if necessary) in pure Java.

I will not go into more details about JNA, as their website already explain it pretty well, and I’ll jump directly to the code which motivated this article : how to get the idle time from the system.

To date, the only JNA implementation I’ve done is for Windows’ GetLastInputInfo method.

To compile and run :

  • Download jna.jar from JNA‘s download page.
  • Go to the directory where jna.jar was saved and create the file Win32IdleTime.java in it.
  • Open cmd.exe and go to the directory. Compile with

    javac -classpath jna.jar Win32IdleTime.java

  • Run with

    java -cp jna.jar;. Win32IdleTime

    (replace “;” by “:” on Unix systems).

    Enjoy !

    (I decline all responsability from it, bla bla bla, copy-paste as you wish, comments are welcome)

    import java.text.DateFormat;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import com.sun.jna.*;
    import com.sun.jna.win32.*;
    /**
     * Utility method to retrieve the idle time on Windows and sample code to test it.
     * JNA shall be present in your classpath for this to work (and compile).
     * @author ochafik
     */
    public class Win32IdleTime {
    	public interface Kernel32 extends StdCallLibrary {
    		Kernel32 INSTANCE = (Kernel32)Native.loadLibrary("kernel32", Kernel32.class);
    		/**
    		 * Retrieves the number of milliseconds that have elapsed since the system was started.
    		 * @see http://msdn2.microsoft.com/en-us/library/ms724408.aspx
    		 * @return number of milliseconds that have elapsed since the system was started.
    		 */
    		public int GetTickCount();
    	};
    	public interface User32 extends StdCallLibrary {
    		User32 INSTANCE = (User32)Native.loadLibrary("user32", User32.class);
    		/**
    		 * Contains the time of the last input.
    		 * @see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/userinput/keyboardinput/keyboardinputreference/keyboardinputstructures/lastinputinfo.asp
    		 */
    		public static class LASTINPUTINFO extends Structure {
    			public int cbSize = 8;
    			/// Tick count of when the last input event was received.
    			public int dwTime;
    		}
    		/**
    		 * Retrieves the time of the last input event.
    		 * @see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/userinput/keyboardinput/keyboardinputreference/keyboardinputfunctions/getlastinputinfo.asp
    		 * @return time of the last input event, in milliseconds
    		 */
    		public boolean GetLastInputInfo(LASTINPUTINFO result);
    	};
    	/**
    	 * Get the amount of milliseconds that have elapsed since the last input event
    	 * (mouse or keyboard)
    	 * @return idle time in milliseconds
    	 */
    	public static int getIdleTimeMillisWin32() {
    		User32.LASTINPUTINFO lastInputInfo = new User32.LASTINPUTINFO();
    		User32.INSTANCE.GetLastInputInfo(lastInputInfo);
    		return Kernel32.INSTANCE.GetTickCount() - lastInputInfo.dwTime;
    	}
    	enum State {
    		UNKNOWN, ONLINE, IDLE, AWAY
    	};
    	public static void main(String[] args) {
    		if (!System.getProperty("os.name").contains("Windows")) {
    			System.err.println("ERROR: Only implemented on Windows");
    			System.exit(1);
    		}
    		State state = State.UNKNOWN;
    		DateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss");
    		for (;;) {
    			int idleSec = getIdleTimeMillisWin32() / 1000;
    			State newState =
    				idleSec < 30 ? State.ONLINE :
    				idleSec > 5 * 60 ? State.AWAY : State.IDLE;
    			if (newState != state) {
    				state = newState;
    				System.out.println(dateFormat.format(new Date()) + " # " + state);
    			}
    			try { Thread.sleep(1000); } catch (Exception ex) {}
    		}
    	}
    }