From http://www.jwz.org/xscreensaver/xscreensaver-5.35.tar.gz
[xscreensaver] / jwxyz / jwxyz-android.c
diff --git a/jwxyz/jwxyz-android.c b/jwxyz/jwxyz-android.c
new file mode 100644 (file)
index 0000000..420cd56
--- /dev/null
@@ -0,0 +1,1211 @@
+/* xscreensaver, Copyright (c) 2016 Jamie Zawinski <jwz@jwz.org>
+ *
+ * Permission to use, copy, modify, distribute, and sell this software and its
+ * documentation for any purpose is hereby granted without fee, provided that
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation.  No representations are made about the suitability of this
+ * software for any purpose.  It is provided "as is" without express or 
+ * implied warranty.
+ *
+ * This file is three related things:
+ *
+ *   - It is the Android-specific C companion to jwxyz-gl.c;
+ *   - It is how C calls into Java to do things that OpenGL does not
+ *     have access to without Java-based APIs;
+ *   - It is how the jwxyz.java class calls into C to run the hacks.
+ */
+
+#include "config.h"
+
+#ifdef HAVE_ANDROID /* whole file */
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+#include <math.h>
+#include <setjmp.h>
+
+#include <GLES/gl.h>
+#include <jni.h>
+#include <android/log.h>
+#include <pthread.h>
+
+#include "screenhackI.h"
+#include "jwxyzI.h"
+#include "jwzglesI.h"
+#include "jwxyz-android.h"
+#include "textclient.h"
+#include "grabscreen.h"
+
+
+#define countof(x) (sizeof(x)/sizeof(*(x)))
+
+extern struct xscreensaver_function_table *xscreensaver_function_table;
+
+struct function_table_entry {
+  const char *progname;
+  struct xscreensaver_function_table *xsft;
+};
+
+#include "gen/function-table.h"
+
+struct event_queue {
+  XEvent event;
+  struct event_queue *next;
+};
+
+static void send_queued_events (struct running_hack *rh);
+
+const char *progname;
+const char *progclass;
+int mono_p = 0;
+
+static JavaVM *global_jvm;
+static jmp_buf jmp_target;
+
+static double current_rotation = 0;
+
+extern void check_gl_error (const char *type);
+
+void
+do_logv(int prio, const char *fmt, va_list args)
+{
+  __android_log_vprint(prio, "xscreensaver", fmt, args);
+
+  /* The idea here is that if the device/emulator dies shortly after a log
+     message, then waiting here for a short while should increase the odds
+     that adb logcat can pick up the message before everything blows up. Of
+     course, doing this means dumping a ton of messages will slow things down
+     significantly.
+  */
+# if 0
+  struct timespec ts;
+  ts.tv_sec = 0;
+  ts.tv_nsec = 25 * 1000000;
+  nanosleep(&ts, NULL);
+# endif
+}
+
+void Log(const char *fmt, ...)
+{
+  va_list args;
+  va_start (args, fmt);
+  Logv(fmt, args);
+  va_end (args);
+}
+
+/* Handle an abort on Android
+   TODO: Test that Android handles aborts properly
+ */
+void
+jwxyz_abort (const char *fmt, ...)
+{
+  /* Send error to Android device log */
+  if (!fmt || !*fmt)
+    fmt = "abort";
+
+  va_list args;
+  va_start (args, fmt);
+  do_logv(ANDROID_LOG_ERROR, fmt, args);
+  va_end (args);
+
+  char buf[10240];
+  va_start (args, fmt);
+  vsprintf (buf, fmt, args);
+  va_end (args);
+
+  JNIEnv *env;
+  (*global_jvm)->AttachCurrentThread (global_jvm, &env, NULL);
+
+  if (! (*env)->ExceptionOccurred(env)) {
+    // If there's already an exception queued, let's just go with that one.
+    // Else, queue a Java exception to be thrown.
+    (*env)->ThrowNew (env, (*env)->FindClass(env, "java/lang/RuntimeException"),
+                      buf);
+  }
+
+  // Nonlocal exit out of the jwxyz code.
+  longjmp (jmp_target, 1);
+}
+
+
+/* We get to keep live references to Java classes in use because the VM can
+   unload a class that isn't being used, which invalidates field and method
+   IDs.
+   https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/design.html#wp17074
+*/
+
+
+// #### only need one var I think
+static size_t classRefCount = 0;
+static jobject globalRefjwxyz, globalRefIterable, globalRefIterator,
+    globalRefMapEntry;
+
+static jfieldID runningHackField;
+static jmethodID iterableIterator, iteratorHasNext, iteratorNext;
+static jmethodID entryGetKey, entryGetValue;
+
+static pthread_mutex_t mutg = PTHREAD_MUTEX_INITIALIZER;
+
+static void screenhack_do_fps (Display *, Window, fps_state *, void *);
+
+
+// Initialized OpenGL and runs the screenhack's init function.
+//
+static void
+doinit (jobject jwxyz_obj, struct running_hack *rh, JNIEnv *env,
+        const struct function_table_entry *chosen, jint api, 
+        jobject defaults, jint w, jint h)
+{
+  if (setjmp (jmp_target)) goto END;  // Jump here from jwxyz_abort and return.
+
+  progname = chosen->progname;
+  rh->xsft = chosen->xsft;
+  rh->api = api;
+  rh->jni_env = env;
+  rh->jobject = jwxyz_obj;  // update this every time we call into C
+
+  (*env)->GetJavaVM (env, &global_jvm);
+
+# undef ya_rand_init  // This is the one and only place it is allowed
+  ya_rand_init (0);
+
+  Window wnd = (Window) calloc(1, sizeof(*wnd));
+  wnd->window.rh = rh;
+  wnd->frame.width = w;
+  wnd->frame.height = h;
+  wnd->type = WINDOW;
+
+  rh->egl_window_ctx = eglGetCurrentContext();
+  Assert(rh->egl_window_ctx != EGL_NO_CONTEXT, "doinit: EGL_NO_CONTEXT");
+
+  wnd->egl_surface = eglGetCurrentSurface(EGL_DRAW);
+  Assert(eglGetCurrentSurface(EGL_READ) == wnd->egl_surface,
+         "doinit: EGL_READ != EGL_DRAW");
+
+  rh->egl_display = eglGetCurrentDisplay();
+  Assert(rh->egl_display != EGL_NO_DISPLAY, "doinit: EGL_NO_DISPLAY");
+
+  EGLint config_attribs[3];
+  config_attribs[0] = EGL_CONFIG_ID;
+  eglQueryContext(rh->egl_display, rh->egl_window_ctx, EGL_CONFIG_ID,
+                  &config_attribs[1]);
+  config_attribs[2] = EGL_NONE;
+
+  EGLint num_config;
+  eglChooseConfig(rh->egl_display, config_attribs,
+                  &rh->egl_config, 1, &num_config);
+  Assert(num_config == 1, "no EGL config chosen");
+
+  rh->egl_xlib_ctx = eglCreateContext(rh->egl_display, rh->egl_config,
+                                      EGL_NO_CONTEXT, NULL);
+  Assert(rh->egl_xlib_ctx != EGL_NO_CONTEXT, "doinit: EGL_NO_CONTEXT");
+  Assert(rh->egl_xlib_ctx != rh->egl_window_ctx, "Only one context here?!");
+
+  rh->window = wnd;
+  rh->dpy = jwxyz_make_display(wnd);
+  Assert(wnd == XRootWindow(rh->dpy, 0), "Wrong root window.");
+  // TODO: Zero looks right, but double-check that is the right number
+
+  progclass = rh->xsft->progclass;
+
+  if ((*env)->ExceptionOccurred(env)) abort();
+  jwzgles_reset();
+
+  // This has to come before resource processing. It does not do graphics.
+  if (rh->xsft->setup_cb)
+    rh->xsft->setup_cb(rh->xsft, rh->xsft->setup_arg);
+
+  if ((*env)->ExceptionOccurred(env)) abort();
+
+  // Load the defaults.
+  // Unceremoniously stolen from [PrefsReader defaultsToDict:].
+
+  jclass     c = (*env)->GetObjectClass (env, defaults);
+  jmethodID  m = (*env)->GetMethodID (env, c, "put",
+                 "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
+  if (! m) abort();
+  if ((*env)->ExceptionOccurred(env)) abort();
+
+  const struct { const char *key, *val; } default_defaults[] = {
+    { "doubleBuffer", "false" },
+    { "multiSample",  "false" },
+    { "texFontCacheSize", "30" },
+    { "textMode", "date" },
+    { "textURL",
+      "https://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss" },
+    { "grabDesktopImages",  "true" },
+    { "chooseRandomImages", "true" },
+  };
+
+  for (int i = 0; i < countof(default_defaults); i++) {
+    const char *key = default_defaults[i].key;
+    const char *val = default_defaults[i].val;
+    char *key2 = malloc (strlen(progname) + strlen(key) + 2);
+    strcpy (key2, progname);
+    strcat (key2, "_");
+    strcat (key2, key);
+
+    // defaults.put(key2, val);
+    jstring jkey = (*env)->NewStringUTF (env, key2);
+    jstring jval = (*env)->NewStringUTF (env, val);
+    (*env)->CallObjectMethod (env, defaults, m, jkey, jval);
+    (*env)->DeleteLocalRef (env, jkey);
+    (*env)->DeleteLocalRef (env, jval);
+    // Log ("default0: \"%s\" = \"%s\"", key2, val);
+    free (key2);
+  }
+
+  const char *const *defs = rh->xsft->defaults;
+  while (*defs) {
+    char *line = strdup (*defs);
+    char *key, *val;
+    key = line;
+    while (*key == '.' || *key == '*' || *key == ' ' || *key == '\t')
+      key++;
+    val = key;
+    while (*val && *val != ':')
+      val++;
+    if (*val != ':') abort();
+    *val++ = 0;
+    while (*val == ' ' || *val == '\t')
+      val++;
+
+    unsigned long L = strlen(val);
+    while (L > 0 && (val[L-1] == ' ' || val[L-1] == '\t'))
+      val[--L] = 0;
+
+    char *key2 = malloc (strlen(progname) + strlen(key) + 2);
+    strcpy (key2, progname);
+    strcat (key2, "_");
+    strcat (key2, key);
+
+    // defaults.put(key2, val);
+    jstring jkey = (*env)->NewStringUTF (env, key2);
+    jstring jval = (*env)->NewStringUTF (env, val);
+    (*env)->CallObjectMethod (env, defaults, m, jkey, jval);
+    (*env)->DeleteLocalRef (env, jkey);
+    (*env)->DeleteLocalRef (env, jval);
+    // Log ("default: \"%s\" = \"%s\"", key2, val);
+    free (key2);
+    free (line);
+    defs++;
+  }
+
+  (*env)->DeleteLocalRef (env, c);
+  if ((*env)->ExceptionOccurred(env)) abort();
+
+ END: ;
+}
+
+
+#undef DEBUG_FPS
+
+// Animates a single frame of the current hack.
+//
+static void
+drawXScreenSaver (JNIEnv *env, struct running_hack *rh)
+{
+  double now = 0;
+# ifdef DEBUG_FPS
+  double fps0=0, fps1=0, fps2=0, fps3=0, fps4=0;
+# endif
+
+  if (setjmp (jmp_target)) goto END;  // Jump here from jwxyz_abort and return.
+
+  /* There is some kind of weird redisplay race condition between Settings
+     and the launching hack: e.g., Abstractile does XClearWindow at init,
+     but the screen is getting filled with random bits.  So let's wait a
+     few frames before really starting up.
+   */
+  if (++rh->frame_count < 8)
+    goto END;
+
+  /* Some of the screen hacks want to delay for long periods, and letting
+     the framework run the update function at 30 FPS when it really wanted
+     half a minute between frames would be bad.  So instead, we assume that
+     the framework's animation timer might fire whenever, but we only invoke
+     the screen hack's "draw frame" method when enough time has expired.
+  */
+  struct timeval tv;
+  gettimeofday (&tv, 0);
+  now = tv.tv_sec + (tv.tv_usec / 1000000.0);
+# ifdef DEBUG_FPS
+  fps0 = fps1 = fps2 = fps3 = fps4 = now;
+#endif
+  if (now < rh->next_frame_time) goto END;
+
+  prepare_context(rh);
+
+# ifdef DEBUG_FPS
+  gettimeofday (&tv, 0);
+  fps1 = tv.tv_sec + (tv.tv_usec / 1000000.0);
+# endif
+
+  // The init function might do graphics (e.g. XClearWindow) so it has
+  // to be run from inside onDrawFrame, not onSurfaceChanged.
+
+  if (! rh->initted_p) {
+
+    void *(*init_cb) (Display *, Window, void *) =
+      (void *(*)(Display *, Window, void *)) rh->xsft->init_cb;
+
+    unsigned int bg =
+      get_pixel_resource (rh->dpy, 0, "background", "Background");
+    XSetWindowBackground (rh->dpy, rh->window, bg);
+    XClearWindow (rh->dpy, rh->window);
+
+    rh->closure = init_cb (rh->dpy, rh->window, rh->xsft->setup_arg);
+    rh->initted_p = True;
+
+    rh->ignore_rotation_p =
+      (rh->api == API_XLIB &&
+       get_boolean_resource (rh->dpy, "ignoreRotation", "IgnoreRotation"));
+
+    if (get_boolean_resource (rh->dpy, "doFPS", "DoFPS")) {
+      rh->fpst = fps_init (rh->dpy, rh->window);
+      if (! rh->xsft->fps_cb) rh->xsft->fps_cb = screenhack_do_fps;
+    } else {
+      rh->fpst = NULL;
+      rh->xsft->fps_cb = 0;
+    }
+
+    if ((*env)->ExceptionOccurred(env)) abort();
+  }
+
+# ifdef DEBUG_FPS
+  gettimeofday (&tv, 0);
+  fps2 = tv.tv_sec + (tv.tv_usec / 1000000.0);
+# endif
+
+  // Apparently events don't come in on the drawing thread, and JNI flips
+  // out.  So we queue them there and run them here.
+  send_queued_events (rh);
+
+# ifdef DEBUG_FPS
+  gettimeofday (&tv, 0);
+  fps3 = tv.tv_sec + (tv.tv_usec / 1000000.0);
+# endif
+
+  unsigned long delay = rh->xsft->draw_cb(rh->dpy, rh->window, rh->closure);
+
+# ifdef __arm__
+  /* #### Until we work out why eglMakeCurrent is so slow on ARM. */
+  if (delay <= 40000) delay = 0;
+# endif
+
+
+# ifdef DEBUG_FPS
+  gettimeofday (&tv, 0);
+  fps4 = tv.tv_sec + (tv.tv_usec / 1000000.0);
+# endif
+  if (rh->fpst && rh->xsft->fps_cb)
+    rh->xsft->fps_cb (rh->dpy, rh->window, rh->fpst, rh->closure);
+
+  gettimeofday (&tv, 0);
+  now = tv.tv_sec + (tv.tv_usec / 1000000.0);
+  rh->next_frame_time = now + (delay / 1000000.0);
+
+ END: ;
+
+# ifdef DEBUG_FPS
+  Log("## FPS prep = %-6d init = %-6d events = %-6d draw = %-6d fps = %-6d\n",
+      (int) ((fps1-fps0)*1000000),
+      (int) ((fps2-fps1)*1000000),
+      (int) ((fps3-fps2)*1000000),
+      (int) ((fps4-fps3)*1000000),
+      (int) ( (now-fps4)*1000000));
+# endif
+}
+
+
+// Extracts the C structure that is stored in the jwxyz Java object.
+static struct running_hack *
+getRunningHack (JNIEnv *env, jobject thiz)
+{
+  jlong result = (*env)->GetLongField (env, thiz, runningHackField);
+  struct running_hack *rh = (struct running_hack *)(intptr_t)result;
+  if (rh)
+    rh->jobject = thiz;  // update this every time we call into C
+  return rh;
+}
+
+// Look up a class and mark it global in the provided variable.
+static jclass
+acquireClass (JNIEnv *env, const char *className, jobject *globalRef)
+{
+  jclass clazz = (*env)->FindClass(env, className);
+  *globalRef = (*env)->NewGlobalRef(env, clazz);
+  return clazz;
+}
+
+
+/* Note: to find signature strings for native methods:
+   cd ./project/xscreensaver/build/intermediates/classes/debug/
+   javap -s -p org.jwz.xscreensaver.jwxyz
+ */
+
+
+// Implementation of jwxyz's nativeInit Java method.
+//
+JNIEXPORT void JNICALL
+Java_org_jwz_xscreensaver_jwxyz_nativeInit (JNIEnv *env, jobject thiz,
+                                            jstring jhack, jint api,
+                                            jobject defaults,
+                                            jint w, jint h)
+{
+  pthread_mutex_lock(&mutg);
+
+  struct running_hack *rh = calloc(1, sizeof(struct running_hack));
+
+  if ((*env)->ExceptionOccurred(env)) abort();
+
+  // #### simplify
+  if (!classRefCount) {
+    jclass classjwxyz = (*env)->GetObjectClass(env, thiz);
+    globalRefjwxyz = (*env)->NewGlobalRef(env, classjwxyz);
+    runningHackField = (*env)->GetFieldID
+      (env, classjwxyz, "nativeRunningHackPtr", "J");
+    if ((*env)->ExceptionOccurred(env)) abort();
+
+    jclass classIterable =
+      acquireClass(env, "java/lang/Iterable", &globalRefIterable);
+    iterableIterator = (*env)->GetMethodID
+      (env, classIterable, "iterator", "()Ljava/util/Iterator;");
+    if ((*env)->ExceptionOccurred(env)) abort();
+
+    jclass classIterator =
+      acquireClass(env, "java/util/Iterator", &globalRefIterator);
+    iteratorHasNext = (*env)->GetMethodID
+      (env, classIterator, "hasNext", "()Z");
+    iteratorNext = (*env)->GetMethodID
+      (env, classIterator, "next", "()Ljava/lang/Object;");
+    if ((*env)->ExceptionOccurred(env)) abort();
+
+    jclass classMapEntry =
+      acquireClass(env, "java/util/Map$Entry", &globalRefMapEntry);
+    entryGetKey = (*env)->GetMethodID
+      (env, classMapEntry, "getKey", "()Ljava/lang/Object;");
+    entryGetValue = (*env)->GetMethodID
+      (env, classMapEntry, "getValue", "()Ljava/lang/Object;");
+    if ((*env)->ExceptionOccurred(env)) abort();
+  }
+
+  ++classRefCount;
+
+  // Store the C struct into the Java object.
+  (*env)->SetLongField(env, thiz, runningHackField, (jlong)(intptr_t)rh);
+
+  // TODO: Sort the list so binary search works.
+  const char *hack =(*env)->GetStringUTFChars(env, jhack, NULL);
+
+  int chosen = 0;
+  for (;;) {
+    if (!chosen == countof(function_table)) {
+      Log ("Hack not found: %s", hack);
+      abort();
+    }
+    if (!strcmp(function_table[chosen].progname, hack))
+      break;
+    chosen++;
+  }
+
+  (*env)->ReleaseStringUTFChars(env, jhack, hack);
+
+  doinit (thiz, rh, env, &function_table[chosen], api, defaults, w, h);
+
+  pthread_mutex_unlock(&mutg);
+}
+
+
+JNIEXPORT void JNICALL
+Java_org_jwz_xscreensaver_jwxyz_nativeResize (JNIEnv *env, jobject thiz,
+                                              jint w, jint h, jdouble rot)
+{
+  pthread_mutex_lock(&mutg);
+  if (setjmp (jmp_target)) goto END;  // Jump here from jwxyz_abort and return.
+
+  current_rotation = rot;
+
+  Log ("native rotation: %f", current_rotation);
+
+  struct running_hack *rh = getRunningHack(env, thiz);
+
+  Window wnd = rh->window;
+  wnd->frame.x = 0;
+  wnd->frame.y = 0;
+  wnd->frame.width  = w;
+  wnd->frame.height = h;
+
+  glViewport (0, 0, w, h);
+
+  if (ignore_rotation_p(rh->dpy) &&
+      rot != 0 && rot != 180 && rot != -180) {
+    int swap = w;
+    w = h;
+    h = swap;
+    wnd->frame.width  = w;
+    wnd->frame.height = h;
+  }
+
+  jwxyz_window_resized (rh->dpy);
+  if (rh->initted_p)
+    rh->xsft->reshape_cb (rh->dpy, rh->window, rh->closure,
+                          wnd->frame.width, wnd->frame.height);
+
+  if (rh->api == API_GL) {
+    glMatrixMode (GL_PROJECTION);
+    glRotatef (-rot, 0, 0, 1);
+    glMatrixMode (GL_MODELVIEW);
+  }
+
+ END:
+  pthread_mutex_unlock(&mutg);
+}
+
+
+JNIEXPORT void JNICALL
+Java_org_jwz_xscreensaver_jwxyz_nativeRender (JNIEnv *env, jobject thiz)
+{
+  pthread_mutex_lock(&mutg);
+  struct running_hack *rh = getRunningHack(env, thiz);
+  drawXScreenSaver(env, rh);
+  pthread_mutex_unlock(&mutg);
+}
+
+
+// TODO: Check Java side is calling this properly
+JNIEXPORT void JNICALL
+Java_org_jwz_xscreensaver_jwxyz_nativeDone (JNIEnv *env, jobject thiz)
+{
+  pthread_mutex_lock(&mutg);
+  if (setjmp (jmp_target)) goto END;  // Jump here from jwxyz_abort and return.
+
+  struct running_hack *rh = getRunningHack(env, thiz);
+
+  prepare_context (rh);
+
+  if (rh->initted_p)
+    rh->xsft->free_cb (rh->dpy, rh->window, rh->closure);
+  jwxyz_free_display(rh->dpy);
+
+  free(rh);
+  (*env)->SetLongField(env, thiz, runningHackField, 0);
+
+  --classRefCount;
+  if (!classRefCount) {
+    (*env)->DeleteGlobalRef(env, globalRefjwxyz);
+    (*env)->DeleteGlobalRef(env, globalRefIterable);
+    (*env)->DeleteGlobalRef(env, globalRefIterator);
+    (*env)->DeleteGlobalRef(env, globalRefMapEntry);
+  }
+
+ END:
+  pthread_mutex_unlock(&mutg);
+}
+
+
+static int
+send_event (struct running_hack *rh, XEvent *e)
+{
+  // Assumes mutex is locked and context is prepared
+
+  int *xP = 0, *yP = 0;
+  switch (e->xany.type) {
+  case ButtonPress: case ButtonRelease:
+    xP = &e->xbutton.x;
+    yP = &e->xbutton.y;
+    break;
+  case MotionNotify:
+    xP = &e->xmotion.x;
+    yP = &e->xmotion.y;
+    break;
+  }
+
+  // Rotate the coordinates in the events to match the pixels.
+  if (xP) {
+    if (ignore_rotation_p (rh->dpy)) {
+      Window win = XRootWindow (rh->dpy, 0);
+      int w = win->frame.width;
+      int h = win->frame.height;
+      int swap;
+      switch ((int) current_rotation) {
+      case 180: case -180:                             // #### untested
+        *xP = w - *xP;
+        *yP = h - *yP;
+        break;
+      case 90: case -270:
+        swap = *xP; *xP = *yP; *yP = swap;
+        *yP = h - *yP;
+        break;
+      case -90: case 270:                              // #### untested
+        swap = *xP; *xP = *yP; *yP = swap;
+        *xP = w - *xP;
+        break;
+      }
+    }
+
+    rh->window->window.last_mouse_x = *xP;
+    rh->window->window.last_mouse_y = *yP;
+  }
+
+  return (rh->xsft->event_cb
+          ? rh->xsft->event_cb (rh->dpy, rh->window, rh->closure, e)
+          : 0);
+}
+
+
+static void
+send_queued_events (struct running_hack *rh)
+{
+  struct event_queue *event, *next;
+  if (! rh->event_queue) return;
+  for (event = rh->event_queue, next = event->next;
+       event;
+       event = next, next = (event ? event->next : 0)) {
+    if (! send_event (rh, &event->event)) {
+      // #### flash the screen or something
+    }
+    free (event);
+  }
+  rh->event_queue = 0;
+}
+
+
+static void
+queue_event (JNIEnv *env, jobject thiz, XEvent *e)
+{
+  pthread_mutex_lock (&mutg);
+  struct running_hack *rh = getRunningHack (env, thiz);
+  struct event_queue *q = (struct event_queue *) malloc (sizeof(*q));
+  memcpy (&q->event, e, sizeof(*e));
+  q->next = 0;
+
+  // Put it at the end.
+  struct event_queue *oq;
+  for (oq = rh->event_queue; oq && oq->next; oq = oq->next)
+    ;
+  if (oq)
+    oq->next = q;
+  else
+    rh->event_queue = q;
+
+  pthread_mutex_unlock (&mutg);
+}
+
+
+JNIEXPORT void JNICALL
+Java_org_jwz_xscreensaver_jwxyz_sendButtonEvent (JNIEnv *env, jobject thiz,
+                                                 int x, int y, jboolean down)
+{
+  XEvent e;
+  memset (&e, 0, sizeof(e));
+  e.xany.type = (down ? ButtonPress : ButtonRelease);
+  e.xbutton.button = Button1;
+  e.xbutton.x = x;
+  e.xbutton.y = y;
+  queue_event (env, thiz, &e);
+}
+
+JNIEXPORT void JNICALL
+Java_org_jwz_xscreensaver_jwxyz_sendMotionEvent (JNIEnv *env, jobject thiz,
+                                                 int x, int y)
+{
+  XEvent e;
+  memset (&e, 0, sizeof(e));
+  e.xany.type = MotionNotify;
+  e.xmotion.x = x;
+  e.xmotion.y = y;
+  queue_event (env, thiz, &e);
+}
+
+JNIEXPORT void JNICALL
+Java_org_jwz_xscreensaver_jwxyz_sendKeyEvent (JNIEnv *env, jobject thiz,
+                                              jboolean down_p, 
+                                              int code, int mods)
+{
+  XEvent e;
+  memset (&e, 0, sizeof(e));
+  e.xkey.keycode = code;
+  e.xkey.state = code;
+  e.xany.type = (down_p ? KeyPress : KeyRelease);
+  queue_event (env, thiz, &e);
+  e.xany.type = KeyRelease;
+  queue_event (env, thiz, &e);
+}
+
+
+
+/***************************************************************************
+  Backend functions for jwxyz-gl.c
+ */
+
+void
+prepare_context (struct running_hack *rh)
+{
+  Window w = rh->window;
+  eglMakeCurrent (rh->egl_display, w->egl_surface, w->egl_surface,
+                  rh->egl_window_ctx);
+  rh->current_drawable = rh->window;
+}
+
+void
+jwxyz_bind_drawable (Display *dpy, Window w, Drawable d)
+{
+  struct running_hack *rh = w->window.rh;
+  JNIEnv *env = w->window.rh->jni_env;
+  if ((*env)->ExceptionOccurred(env)) abort();
+  if (rh->current_drawable != d) {
+    EGLContext ctx = d == w ? rh->egl_window_ctx : rh->egl_xlib_ctx;
+    eglMakeCurrent (rh->egl_display, d->egl_surface, d->egl_surface, ctx);
+    // Log("%p %p %p %p %p", rh->egl_display, d->egl_surface, ctx, w, d);
+    rh->current_drawable = d;
+    jwxyz_assert_gl ();
+
+    if (d != w) {
+      glViewport (0, 0, d->frame.width, d->frame.height);
+      jwxyz_set_matrices (dpy, d->frame.width, d->frame.height, False);
+    }
+  }
+}
+
+
+const XRectangle *
+jwxyz_frame (Drawable d)
+{
+  return &d->frame;
+}
+
+
+unsigned int
+jwxyz_drawable_depth (Drawable d)
+{
+  return (d->type == WINDOW
+          ? visual_depth (NULL, NULL)
+          : d->pixmap.depth);
+}
+
+
+void
+jwxyz_get_pos (Window w, XPoint *xvpos, XPoint *xp)
+{
+  xvpos->x = 0;
+  xvpos->y = 0;
+
+  if (xp) {
+    xp->x = w->window.last_mouse_x;
+    xp->y = w->window.last_mouse_y;
+  }
+}
+
+
+static void
+screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
+{
+  fps_compute (fpst, 0, -1);
+  fps_draw (fpst);
+}
+
+
+void
+jwxyz_copy_area (Display *dpy, Drawable src, Drawable dst, GC gc,
+                 int src_x, int src_y, unsigned int width, unsigned int height,
+                 int dst_x, int dst_y)
+{
+#if 0
+  // Hilarious display corruption ahoy!
+  jwxyz_gl_copy_area_copy_tex_image (dpy, src, dst, gc, src_x, src_y,
+                                     width, height, dst_x, dst_y);
+#else
+  jwxyz_gl_copy_area_read_pixels (dpy, src, dst, gc, src_x, src_y,
+                                  width, height, dst_x, dst_y);
+#endif
+  jwxyz_assert_gl ();
+}
+
+
+void
+jwxyz_assert_drawable (Window main_window, Drawable d)
+{
+  check_gl_error("jwxyz_assert_drawable");
+}
+
+
+void
+jwxyz_assert_gl (void)
+{
+  check_gl_error("jwxyz_assert_gl");
+}
+
+
+Pixmap
+XCreatePixmap (Display *dpy, Drawable d,
+               unsigned int width, unsigned int height, unsigned int depth)
+{
+  // See also:
+  // https://web.archive.org/web/20140213220709/http://blog.vlad1.com/2010/07/01/how-to-go-mad-while-trying-to-render-to-a-texture/
+  // https://software.intel.com/en-us/articles/using-opengl-es-to-accelerate-apps-with-legacy-2d-guis
+  // https://www.khronos.org/registry/egl/extensions/ANDROID/EGL_ANDROID_image_native_buffer.txt
+
+  Window win = XRootWindow(dpy, 0);
+
+  Pixmap p = malloc(sizeof(*p));
+  p->type = PIXMAP;
+  p->frame.x = 0;
+  p->frame.y = 0;
+  p->frame.width = width;
+  p->frame.height = height;
+
+  Assert(depth == 1 || depth == visual_depth(NULL, NULL),
+         "XCreatePixmap: bad depth");
+  p->pixmap.depth = depth;
+
+  // Native EGL is Android 2.3/API 9. EGL in Java is available from API 1.
+  struct running_hack *rh = win->window.rh;
+  EGLint attribs[5];
+  attribs[0] = EGL_WIDTH;
+  attribs[1] = width;
+  attribs[2] = EGL_HEIGHT;
+  attribs[3] = height;
+  attribs[4] = EGL_NONE;
+  p->egl_surface = eglCreatePbufferSurface(rh->egl_display, rh->egl_config,
+                                           attribs);
+  Assert(p->egl_surface != EGL_NO_SURFACE,
+         "XCreatePixmap: got EGL_NO_SURFACE");
+
+  jwxyz_bind_drawable (dpy, win, p);
+  glClearColor (frand(1), frand(1), frand(1), 0);
+  glClear (GL_COLOR_BUFFER_BIT);
+
+  return p;
+}
+
+
+int
+XFreePixmap (Display *d, Pixmap p)
+{
+  struct running_hack *rh = XRootWindow(d, 0)->window.rh;
+  if (rh->current_drawable == p)
+    rh->current_drawable = NULL;
+  eglDestroySurface(rh->egl_display, p->egl_surface);
+  free (p);
+  return 0;
+}
+
+
+double
+current_device_rotation (void)
+{
+  return current_rotation;
+}
+
+Bool
+ignore_rotation_p (Display *dpy)
+{
+  struct running_hack *rh = XRootWindow(dpy, 0)->window.rh;
+  return rh->ignore_rotation_p;
+}
+
+
+char *
+get_string_resource (Display *dpy, char *name, char *class)
+{
+  Window window = RootWindow (dpy, 0);
+  JNIEnv *env = window->window.rh->jni_env;
+  jobject obj = window->window.rh->jobject;
+
+  if ((*env)->ExceptionOccurred(env)) abort();
+  jstring jstr = (*env)->NewStringUTF (env, name);
+  jclass     c = (*env)->GetObjectClass (env, obj);
+  jmethodID  m = (*env)->GetMethodID (env, c, "getStringResource",
+                           "(Ljava/lang/String;)Ljava/lang/String;");
+  if ((*env)->ExceptionOccurred(env)) abort();
+
+  jstring jvalue = (m
+                  ? (*env)->CallObjectMethod (env, obj, m, jstr)
+                  : NULL);
+  (*env)->DeleteLocalRef (env, c);
+  (*env)->DeleteLocalRef (env, jstr);
+  char *ret = 0;
+  if (jvalue) {
+    const char *cvalue = (*env)->GetStringUTFChars (env, jvalue, 0);
+    ret = strdup (cvalue);
+    (*env)->ReleaseStringUTFChars (env, jvalue, cvalue);
+  }
+
+  Log("pref %s = %s", name, (ret ? ret : "(null)"));
+  return ret;
+}
+
+
+/* Returns the contents of the URL. */
+char *
+textclient_mobile_url_string (Display *dpy, const char *url)
+{
+  Window window = RootWindow (dpy, 0);
+  JNIEnv *env = window->window.rh->jni_env;
+  jobject obj = window->window.rh->jobject;
+
+  jstring jstr  = (*env)->NewStringUTF (env, url);
+  jclass      c = (*env)->GetObjectClass (env, obj);
+  jmethodID   m = (*env)->GetMethodID (env, c, "loadURL",
+                            "(Ljava/lang/String;)Ljava/nio/ByteBuffer;");
+  if ((*env)->ExceptionOccurred(env)) abort();
+  jobject buf = (m
+                 ? (*env)->CallObjectMethod (env, obj, m, jstr)
+                 : NULL);
+  (*env)->DeleteLocalRef (env, c);
+  (*env)->DeleteLocalRef (env, jstr);
+
+  char *body = (char *) (buf ? (*env)->GetDirectBufferAddress (env, buf) : 0);
+  char *body2;
+  if (body) {
+    int L = (*env)->GetDirectBufferCapacity (env, buf);
+    body2 = malloc (L + 1);
+    memcpy (body2, body, L);
+    body2[L] = 0;
+  } else {
+    body2 = strdup ("ERROR");
+  }
+
+  if (buf)
+    (*env)->DeleteLocalRef (env, buf);
+
+  return body2;
+}
+
+
+void *
+jwxyz_load_native_font (Display *dpy, const char *name,
+                        char **native_name_ret, float *size_ret,
+                        int *ascent_ret, int *descent_ret)
+{
+  Window window = RootWindow (dpy, 0);
+  JNIEnv *env = window->window.rh->jni_env;
+  jobject obj = window->window.rh->jobject;
+
+  jstring jstr = (*env)->NewStringUTF (env, name);
+  jclass     c = (*env)->GetObjectClass (env, obj);
+  jmethodID  m = (*env)->GetMethodID (env, c, "loadFont",
+                           "(Ljava/lang/String;)[Ljava/lang/Object;");
+  if ((*env)->ExceptionOccurred(env)) abort();
+
+  jobjectArray array = (m
+                        ? (*env)->CallObjectMethod (env, obj, m, jstr)
+                        : NULL);
+  (*env)->DeleteLocalRef (env, c);
+  (*env)->DeleteLocalRef (env, jstr);
+  jstr = 0;
+
+  if (array) {
+    jobject font = (*env)->GetObjectArrayElement (env, array, 0);
+    jobject name = (jstring) ((*env)->GetObjectArrayElement (env, array, 1));
+    jobject size = (*env)->GetObjectArrayElement (env, array, 2);
+    jobject asc  = (*env)->GetObjectArrayElement (env, array, 3);
+    jobject desc = (*env)->GetObjectArrayElement (env, array, 4);
+    if ((*env)->ExceptionOccurred(env)) abort();
+
+    const char *cname = (*env)->GetStringUTFChars (env, name, 0);
+    *native_name_ret = strdup (cname);
+    (*env)->ReleaseStringUTFChars (env, name, cname);
+
+    c = (*env)->GetObjectClass(env, font);
+    m = (*env)->GetMethodID (env, c, "longValue", "()J");
+    long font_id = (*env)->CallLongMethod (env, font, m);
+    if ((*env)->ExceptionOccurred(env)) abort();
+
+    c = (*env)->GetObjectClass(env, size);
+    m = (*env)->GetMethodID (env, c, "floatValue", "()F");
+    if ((*env)->ExceptionOccurred(env)) abort();
+
+    *size_ret    =       (*env)->CallFloatMethod (env, size, m);
+    *ascent_ret  = (int) (*env)->CallFloatMethod (env, asc,  m);
+    *descent_ret = (int) (*env)->CallFloatMethod (env, desc, m);
+
+    return (void *) font_id;
+  } else {
+    return 0;
+  }
+}
+
+
+void
+jwxyz_release_native_font (Display *dpy, void *native_font)
+{
+  Window window = RootWindow (dpy, 0);
+  JNIEnv *env = window->window.rh->jni_env;
+  jobject obj = window->window.rh->jobject;
+  if ((*env)->ExceptionOccurred(env)) abort();
+  jclass    c = (*env)->GetObjectClass (env, obj);
+  jmethodID m = (*env)->GetMethodID (env, c, "releaseFont", "(J)V");
+  (*env)->CallVoidMethod (env, obj, m, (jobject) native_font);
+  if ((*env)->ExceptionOccurred(env)) abort();
+}
+
+
+/* If the local reference table fills up, use this to figure out where
+   you missed a call to DeleteLocalRef. */
+/*
+static void dump_reference_tables(JNIEnv *env)
+{
+  jclass c = (*env)->FindClass(env, "dalvik/system/VMDebug");
+  jmethodID m = (*env)->GetStaticMethodID (env, c, "dumpReferenceTables",
+                                           "()V");
+  (*env)->CallStaticVoidMethod (env, c, m);
+  (*env)->DeleteLocalRef (env, c);
+}
+*/
+
+
+// Returns the metrics of the multi-character, single-line UTF8 or Latin1
+// string.  If pixmap_ret is provided, also renders the text.
+//
+void
+jwxyz_render_text (Display *dpy, void *native_font,
+                   const char *str, size_t len, int utf8,
+                   XCharStruct *cs, char **pixmap_ret)
+{
+  Window window = RootWindow (dpy, 0);
+  JNIEnv *env = window->window.rh->jni_env;
+  jobject obj = window->window.rh->jobject;
+
+  char *s2;
+
+  if (utf8) {
+    s2 = malloc (len + 1);
+    memcpy (s2, str, len);
+    s2[len] = 0;
+  } else {     // Convert Latin1 to UTF8
+    s2 = malloc (len * 2 + 1);
+    unsigned char *s3 = (unsigned char *) s2;
+    int i;
+    for (i = 0; i < len; i++) {
+      unsigned char c = ((unsigned char *) str)[i];
+      if (! (c & 0x80)) {
+        *s3++ = c;
+      } else {
+        *s3++ = (0xC0 | (0x03 & (c >> 6)));
+        *s3++ = (0x80 | (0x3F & c));
+      }
+    }
+    *s3 = 0;
+  }
+
+  jstring jstr  = (*env)->NewStringUTF (env, s2);
+  jclass      c = (*env)->GetObjectClass (env, obj);
+  jmethodID   m = (*env)->GetMethodID (env, c, "renderText",
+                            "(JLjava/lang/String;Z)Ljava/nio/ByteBuffer;");
+  if ((*env)->ExceptionOccurred(env)) abort();
+  jobject buf =
+    (m
+     ? (*env)->CallObjectMethod (env, obj, m,
+                                 (jlong) (long) native_font,
+                                 jstr,
+                                 (pixmap_ret ? JNI_TRUE : JNI_FALSE))
+     : NULL);
+  (*env)->DeleteLocalRef (env, c);
+  (*env)->DeleteLocalRef (env, jstr);
+  free (s2);
+
+  if ((*env)->ExceptionOccurred(env)) abort();
+  unsigned char *bits = (unsigned char *)
+    (buf ? (*env)->GetDirectBufferAddress (env, buf) : 0);
+  if (bits) {
+    int i = 0;
+    int L = (*env)->GetDirectBufferCapacity (env, buf);
+    if (L < 10) abort();
+    cs->lbearing = (bits[i] << 8) | (bits[i+1] & 0xFF); i += 2;
+    cs->rbearing = (bits[i] << 8) | (bits[i+1] & 0xFF); i += 2;
+    cs->width    = (bits[i] << 8) | (bits[i+1] & 0xFF); i += 2;
+    cs->ascent   = (bits[i] << 8) | (bits[i+1] & 0xFF); i += 2;
+    cs->descent  = (bits[i] << 8) | (bits[i+1] & 0xFF); i += 2;
+
+    if (pixmap_ret) {
+      char *pix = malloc (L - i);
+      if (! pix) abort();
+      memcpy (pix, bits + i, L - i);
+      *pixmap_ret = pix;
+    }
+  } else {
+    memset (cs, 0, sizeof(*cs));
+    if (pixmap_ret)
+      *pixmap_ret = 0;
+  }
+
+  if (buf)
+    (*env)->DeleteLocalRef (env, buf);
+}
+
+
+/* Called from utils/grabclient.c */
+char *
+jwxyz_load_random_image (Display *dpy,
+                         int *width_ret, int *height_ret,
+                         char **name_ret)
+{
+  Window window = RootWindow (dpy, 0);
+  struct running_hack *rh = window->window.rh;
+  JNIEnv *env = rh->jni_env;
+  jobject obj = rh->jobject;
+
+  Bool images_p =
+    get_boolean_resource (rh->dpy, "chooseRandomImages", "ChooseRandomImages");
+  Bool grab_p =
+    get_boolean_resource (rh->dpy, "grabDesktopImages", "GrabDesktopImages");
+  Bool rotate_p =
+    get_boolean_resource (rh->dpy, "rotateImages", "RotateImages");
+
+  if (!images_p && !grab_p)
+    return 0;
+
+  if (grab_p && images_p) {
+    grab_p = !(random() & 5);    /* if both, screenshot 1/5th of the time */
+    images_p = !grab_p;
+  }
+
+  jclass      c = (*env)->GetObjectClass (env, obj);
+  jmethodID   m = (*env)->GetMethodID (env, c, 
+                                       (grab_p
+                                        ? "getScreenshot"
+                                        : "loadRandomImage"),
+                                       "(IIZ)Ljava/nio/ByteBuffer;");
+  if ((*env)->ExceptionOccurred(env)) abort();
+  jobject buf = (m
+                 ? (*env)->CallObjectMethod (env, obj, m,
+                                             window->frame.width,
+                                             window->frame.height,
+                                             (rotate_p ? JNI_TRUE : JNI_FALSE))
+                 : NULL);
+  (*env)->DeleteLocalRef (env, c);
+
+  unsigned char *bits = (unsigned char *)
+    (buf ? (*env)->GetDirectBufferAddress (env, buf) : 0);
+
+  if (bits) {
+    int i = 0;
+    int L = (*env)->GetDirectBufferCapacity (env, buf);
+    if (L < 100) abort();
+    int width  = (bits[i] << 8) | (bits[i+1] & 0xFF); i += 2;
+    int height = (bits[i] << 8) | (bits[i+1] & 0xFF); i += 2;
+    char *name = (char *) bits + i;
+    int L2 = strlen (name);
+    i += L2 + 1;
+    if (width * height * 4 != L - i) abort();
+    char *pix = malloc (L - i);
+    if (! pix) abort();
+    memcpy (pix, bits + i, L - i);
+    *width_ret  = width;
+    *height_ret = height;
+    *name_ret   = strdup (name);
+    return (char *) pix;
+  }
+
+  if (buf)
+    (*env)->DeleteLocalRef (env, buf);
+
+  return 0;
+}
+
+#endif /* HAVE_ANDROID */