PDA

View Full Version : Simple Java JNI Tutorial: Global Keypress Capture (Hotkey) using X11



gmatt
July 19th, 2008, 11:57 PM
This tutorial is for Java devs who want to be able to capture key presses globally under linux (specifically ubunutu.) This is of course OS dependent as Java does not allow for such a thing without the use of the JNI (Java Native Interface.)

This tutorial is as self-contained as possible; however, if you would like more information about JNI a good place to start is the documentation at http://java.sun.com/developer/onlineTraining/Programming/JDCBook/jni.html .

The Problem

The problem is that you would like your Java application to detect keys pressed by the user even if your Java application does not have focus. This is not a trivial problem, as I have learned. For security reasons or convenience, the developers of Java have omitted to give Java developers a painless way of capturing system-wide events such as key presses.

The Solution

Upon further investigation, most operating systems do offer developers a means of capturing such events. On windows, developers can use hooks to listen for global (input) events. On linux, developers can use the X11 library to capture gloabl input events. Unfortunately, both of these are not directly accessible through Java code (windows hooks might be.)

Under linux, a developer can write his/her own code using C/C++ X11 libraries. This, of course, is only part of the solution, since then your Java application must be able to work in harmony with the C/C++ code. Fortunately, JNI allows just that.

The Dirty Details

Before we do anything, make sure that you have X11-dev libraries installed with


sudo apt-get install libx11-dev

Let's start with the implementation. First we write a self-contained Java class as follows:


class KeyGrabber {

static {
System.loadLibrary("KeyGrabber");
}

private native void listen();

public static void fire_key_event(){
System.out.println("key pressed (java code)");
}

public static void main(String[] args) {
new KeyGrabber().listen();
}
}


In the Java code the listen() function is labeled native which tells the Java compiler that it is implemented in C/C++ code that it must link. The listen() function will be the X11 code required to listen for global key presses.

The fire_key_event() function is completely implemented in Java. This function will be called from the native function listen() whenever the X11 code detects a global key press.

Next we compile this Java code with:


javac KeyGrabber.java.

To implement the listen() function in C/C++ we must now use JNI. We start by


javah -jni KeyGrabber

which will generate a header file called KeyGrabber.h that looks like


/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class KeyGrabber */

#ifndef _Included_KeyGrabber
#define _Included_KeyGrabber
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: KeyGrabber
* Method: listen
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_KeyGrabber_listen
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

Since we only had one native function in the Java code we only have one corresponding function prototype in the JNI header, namely:


JNIEXPORT void JNICALL Java_KeyGrabber_listen
(JNIEnv *, jobject);

We must implement this function according to this prototype. We can then implement the X11 code necessary to detect global key presses in KeyGrabber.c as follows.


#include <X11/Xlib.h>
#include"KeyGrabber.h"
#include<stdio.h>

JNIEXPORT void JNICALL Java_KeyGrabber_listen
(JNIEnv *env, jobject obj){

printf("starting to listen to key F3 (native code)\n");
jclass cls = (*env)->FindClass(env, "KeyGrabber");
if(cls == NULL){
printf("cannot find class KeyGrabber\n") ;
exit(-1);
}
jmethodID mid = (*env)->GetStaticMethodID(env, cls, "fire_key_event", "()V" );
if(mid ==0){
printf("cannot find method fun\n");
exit(-1);
}

Window root;
XEvent ev;

Display * dpy = XOpenDisplay(0);

if(!dpy) return 1;

root = DefaultRootWindow(dpy);


char * key_string = "F3";

KeyCode key = XKeysymToKeycode(dpy, XStringToKeysym(key_string));

XGrabKey(dpy, key , AnyModifier, root,
True, GrabModeAsync, GrabModeAsync);

for(;;)
{
XNextEvent(dpy, &ev);
if(ev.type == KeyPress && ev.xkey.keycode == key){
(*env)->CallStaticVoidMethod(env,cls,mid);
}
}



printf("leaving c code\n");

return;

}

This code listens for any key press of F3 with any modifier key, and when it detects such a key, calls the Java function "fire_key_event".

Here is a detailed break down of the sections of the code:


jclass cls = (*env)->FindClass(env, "KeyGrabber");

The jclass class is defined in jni.h which is included when you include "KeyGrabber.h". The env pointer that is passed to every native function is essentially pointing to the JVM. You can manipulate anything that the JVM can manipulate.

Our immediate goal is to be able to call the fire_key_event method in KeyGrabber class whenever the X11 code detects a global key press. The first order of business is to get a handle on the KeyGrabber class, which is what this line does.

The next order of business is to get a handle on the fire_key_event method in the KeyGrabber class. That is what


jmethodID mid = (*env)->GetStaticMethodID(env, cls, "fire_key_event", "()V" ); does.

The first three parameters passed to GetStaticMethodID are clear: pointer to the environment (JVM), the class with the method, the name of the method. The last parameter is the signature of the function (this of course is required because of method overloading.) To get the signature of the desired method use the following command:


javap -s KeyGrabber

which for this example gives:


Compiled from "KeyGrabber.java"
class KeyGrabber extends java.lang.Object{
KeyGrabber();
Signature: ()V
public static void fire_key_event();
Signature: ()V
public static void main(java.lang.String[]);
Signature: ([Ljava/lang/String;)V
static {};
Signature: ()V
}


and then you can copy and paste the signature straight from terminal ;).


The next bit of code




Window root;
XEvent ev;

Display * dpy = XOpenDisplay(0);

if(!dpy) return 1;

root = DefaultRootWindow(dpy);


char * key_string = "F3";

KeyCode key = XKeysymToKeycode(dpy, XStringToKeysym(key_string));

XGrabKey(dpy, key , AnyModifier, root,
True, GrabModeAsync, GrabModeAsync);



is the X11 needed to capture global key presses (in this case only key presses of F3 with any modifier.) I won't go into too much detail, but essentially the XGrabKey function call tells X11 that your code is interested in capturing key press of the F3 key with any modifier on display 0 in the root window (meaning globally.) So the following code



for(;;)
{
XNextEvent(dpy, &ev);
if(ev.type == KeyPress && ev.xkey.keycode == key){
(*env)->CallStaticVoidMethod(env,cls,mid);
}
}


is an infinite loop that listens to all the registered events (in this case only F3 key presses.) If the event type is a KeyPress and the keycode is F3 then the C code will use the env pointer to call the static void method fire_key_event in the KeyGrabber class.

And its that straightforward (hopefully.)


The last order of business is to compile everything and get it running.

To compile the C code, it is a bit tricky. Provided you have a valid Java dev installation you can compile it with:



gcc --shared -o libKeyGrabber.so -I<path to java>/include -I<path to java>/include/linux -lX11 KeyGrabber.c <path to java>/jre/lib/i386/server/libjvm.so


Then to run the Java you must make sure that Java can link to the libKeyGrabber.so library that you have just compiled, so use the following command



java -Djava.library.path=. KeyGrabber


and everything should work! (eh hopefully.)


I've probably missed a lot of things so I hope you won't hesitate to post any questions and comments!

qforce
September 14th, 2008, 06:40 PM
First of all, nice tutorial, very well written!

Now here's a problem I ran into while trying to implement it: The call to XNextEvent is blocking, i.e. it stops the entire thread until the specified key is pressed. A real program however often cannot entirely freeze just to wait for the next event, therefore one would normally call KeyGrabber.listen() inside a separate thread.

That's where the headache for me started: When the Java program is terminated, you have to stop the thread that contains the XNextEvent call. However, I just couldn't figure out how to do that. I tried calling XSendEvent from a different thread in order to unblock XNextEvent, but it somehow didn't work.

Any hints that could solve this problem would be appreciated!

subes
October 11th, 2008, 01:44 AM
Thank you for your tutorial, I used your guide and the inspiration of JIntellitype to create my own generic library for this.

It's not feature complete but allows conversion of swing keycodes and modifiers to x11 ones.

Also not all java keycodes have been mapped yet, as of i dont know much about some special keycodes java has. :D

If somebody wants a library to start with and maybe improve a bit, visit the project website on sourceforge!

http://jxgrabkey.sourceforge.net/

qforce
October 12th, 2008, 09:41 AM
@ Subes
Your source code seems to contain some AWT / Swing stuff. Does it work on SWT, too?

Because if it does, you just made my day.

subes
October 12th, 2008, 02:42 PM
Havent tested it on swt and dont know much about swt :D
The awt stuff im using is all about the keycodes and keymodifiers, it should work on any toolkit, as long as you give it the keycodes from the KeyEvent class.
Tho the code is still not final and may have bugs, youre free do help in development ;)
If you need a KeyGrabber textfield, i have one implemented in Coopnet
you can look there for hints about how to use JXGrabKey

(http://coopnet.sourceforge.net)

look in the packages coopnetclient.utils.hotkeys.JXGrabKeyHandler for the jxgrabkey implementation
and coopnetclient.frames.components.KeyGrabberTextFiel d for the grabber

qforce
October 13th, 2008, 07:14 PM
I have downloaded the source code and skimmed through it, and I think it will work on SWT, too. Man, I just couldn't figure out the "XPending" part!](*,)
Now there's only this one little thing I'd like to ask of you: Could you please change the license from GPL to LGPL? I have a EPL program (http://docfetcher.sourceforge.net) over here, and, as we all know, GPL and LGPL are incompatible. Many thanks in advance!

subes
October 15th, 2008, 08:36 PM
ah, right forgot about the restrictions gpl is pulling in ^^
ill change it in the next few days

#edit
ok, changed the license
do you wanna have commit rights to the code? ill make an invite for you

qforce
October 16th, 2008, 06:11 PM
and, as we all know, GPL and LGPL are incompatible. Many thanks in advance!
err, I mean GPL and EPL are imcompatible.

@subes
Concerning the SVN write access: Right now I'm too busy working on my own projects, but I promise I'll send you all the bugs I may find.

subes
October 16th, 2008, 10:26 PM
sure, thats ok too :)
its best if you create tracker items for those

peanutman
January 11th, 2009, 03:16 PM
Great tutorial!

I was just wondering, how would one do this in OSX where there is no running X11 by default? Is there another library for keyboard capture? I could use SDL or something, but there must be a better way?

Arancaytar
March 13th, 2009, 09:38 PM
I have managed to compile a Java class with the native method signature, then a C source that implemented this method. So now I have a hkl.so file and a KeyboardNative.class file, but whenever I try to run the Java code, it exits with an exception saying the library cannot be found.



Directory structure (pwd = /workspace/technology/java/hkl/bin):

./net/ermarian/hkl/keyboard/KeyboardNative.class
./net/ermarian/hkl/keyboard/hkl.so
./hkl.so (just to make sure)




Commands:

java net.ermarian.hkl.keyboard.KeyboardNative

java -Djava.library.path=/workspace/technology/java/hkl/bin net.ermarian.hkl.keyboard.KeyboardNative


Exception occurring both times:



Exception in thread "main" java.lang.UnsatisfiedLinkError: no hkl in java.library.path


What do I need to do to put the hkl.so library in a place where Java can find it?

subes
March 13th, 2009, 10:27 PM
Hi,

check this for guidance, there's also a demo app that should answer you question maybe.

Project website:
http://sourceforge.net/projects/jxgrabkey/

Heres the demo app class:
http://jxgrabkey.svn.sourceforge.net/viewvc/jxgrabkey/trunk/misc/ReleaseFiles/example/JXGrabKeyTest.java?view=markup

I always use System.load(<absolutepath>). The method you use is new to me.

#edit
Linux expects libraries to start with lib as I remember, maybe try to rename your file to "libhkl.so".

xylometazolin
May 21st, 2009, 04:25 PM
When I try to execute the Java application on my system (Ubuntu Jaunty, 64-bit), I get the following output:

starting to listen to key F3 (native code)
X Error of failed request: BadAccess (attempt to access private resource denied)
Major opcode of failed request: 33 (X_GrabKey)
Serial number of failed request: 10
Current serial number in output stream: 10

Looks like the xserver does not allow the direct key access.
How can I enable it?

qforce
May 21st, 2009, 10:39 PM
You should post this on the bug tracker of the JXGrabkey project:
http://sourceforge.net/tracker/?group_id=241993&atid=1117876

(Btw, I'm just a JXGrabkey user, I'm not involved in the project. ):P)

gmatt
October 23rd, 2009, 06:30 PM
When I try to execute the Java application on my system (Ubuntu Jaunty, 64-bit), I get the following output:

starting to listen to key F3 (native code)
X Error of failed request: BadAccess (attempt to access private resource denied)
Major opcode of failed request: 33 (X_GrabKey)
Serial number of failed request: 10
Current serial number in output stream: 10

Looks like the xserver does not allow the direct key access.
How can I enable it?

Sorry for the extremely tardy response, but that error is caused because some other program is listening to that particular key.

jamsiro
January 29th, 2010, 06:32 PM
This program works for me. I modified it to capture a G key instead of F3. However, once the key is grabbed it is not available to any other window in X other than the one that grabbed it. All I want to do is listen to the global key press event, I don't want to capture the key. How would I go about doing this? For example, I want to be able to type a document in OpenOffice and have a script running in a background to capture all of my key strokes.

qforce
January 29th, 2010, 08:53 PM
@ jamsiro:
It's best if you go to the JXGrabKey SourceForge.net page (http://sourceforge.net/projects/jxgrabkey/develop) and post something on one of the trackers (bug reports, feature requests, support requests).

dr_steveb
May 8th, 2012, 06:54 PM
Great tutorial!

However, I found that in order for the code to run on my system, I needed to move the -X11 to the end of the compile line. Without doing that, it did not link in the XOpenDisplay routine and I had runtime errors.

I hope that this may help others!