From http://www.jwz.org/xscreensaver/xscreensaver-5.38.tar.gz
[xscreensaver] / android / xscreensaver / src / org / jwz / xscreensaver / jwxyz.java
1 /* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2  * xscreensaver, Copyright (c) 2016-2017 Jamie Zawinski <jwz@jwz.org>
3  *
4  * Permission to use, copy, modify, distribute, and sell this software and its
5  * documentation for any purpose is hereby granted without fee, provided that
6  * the above copyright notice appear in all copies and that both that
7  * copyright notice and this permission notice appear in supporting
8  * documentation.  No representations are made about the suitability of this
9  * software for any purpose.  It is provided "as is" without express or 
10  * implied warranty.
11  *
12  * This class is how the C implementation of jwxyz calls back into Java
13  * to do things that OpenGL does not have access to without Java-based APIs.
14  * It is the Java companion to jwxyz-android.c and screenhack-android.c.
15  */
16
17 package org.jwz.xscreensaver;
18
19 import java.util.Map;
20 import java.util.HashMap;
21 import java.util.Hashtable;
22 import java.util.ArrayList;
23 import java.util.Random;
24 import android.app.AlertDialog;
25 import android.view.KeyEvent;
26 import android.content.SharedPreferences;
27 import android.content.Context;
28 import android.content.ContentResolver;
29 import android.content.DialogInterface;
30 import android.content.res.AssetManager;
31 import android.graphics.Typeface;
32 import android.graphics.Rect;
33 import android.graphics.Paint;
34 import android.graphics.Paint.FontMetrics;
35 import android.graphics.Bitmap;
36 import android.graphics.Canvas;
37 import android.graphics.Color;
38 import android.graphics.Matrix;
39 import android.net.Uri;
40 import android.view.GestureDetector;
41 import android.view.KeyEvent;
42 import android.view.MotionEvent;
43 import java.net.URL;
44 import java.nio.ByteBuffer;
45 import java.io.File;
46 import java.io.InputStream;
47 import java.io.FileOutputStream;
48 import java.lang.InterruptedException;
49 import java.lang.Runnable;
50 import java.lang.Thread;
51 import java.util.TimerTask;
52 import android.database.Cursor;
53 import android.provider.MediaStore;
54 import android.provider.MediaStore.MediaColumns;
55 import android.media.ExifInterface;
56 import org.jwz.xscreensaver.TTFAnalyzer;
57 import android.util.Log;
58 import android.view.Surface;
59
60
61 public class jwxyz
62   implements GestureDetector.OnGestureListener,
63              GestureDetector.OnDoubleTapListener {
64
65   private class PrefListener
66     implements SharedPreferences.OnSharedPreferenceChangeListener {
67
68     @Override
69     public void onSharedPreferenceChanged (SharedPreferences sharedPreferences, String key)
70     {
71       if (key.startsWith(hack + "_")) {
72         if (render != null) {
73           boolean was_animating;
74           synchronized (render) {
75             was_animating = animating_p;
76           }
77           close();
78           if (was_animating)
79             start();
80         }
81       }
82     }
83   };
84
85   private static class SurfaceLost extends Exception {
86     SurfaceLost () {
87       super("surface lost");
88     }
89
90     SurfaceLost (String detailMessage) {
91       super(detailMessage);
92     }
93   }
94
95   public final static int STYLE_BOLD      = 1;
96   public final static int STYLE_ITALIC    = 2;
97   public final static int STYLE_MONOSPACE = 4;
98
99   public final static int FONT_FAMILY = 0;
100   public final static int FONT_FACE   = 1;
101   public final static int FONT_RANDOM = 2;
102
103   private long nativeRunningHackPtr;
104
105   private String hack;
106   private Context app;
107   private Bitmap screenshot;
108
109   SharedPreferences prefs;
110   SharedPreferences.OnSharedPreferenceChangeListener pref_listener;
111   Hashtable<String, String> defaults = new Hashtable<String, String>();
112
113
114   // Maps font names to either: String (system font) or Typeface (bundled).
115   private Hashtable<String, Object> all_fonts =
116     new Hashtable<String, Object>();
117
118   int width, height;
119   Surface surface;
120   boolean animating_p;
121
122   // Doubles as the mutex controlling width/height/animating_p.
123   private Thread render;
124
125   private Runnable on_quit;
126   boolean button_down_p;
127
128   // These are defined in jwxyz-android.c:
129   //
130   private native long nativeInit (String hack,
131                                   Hashtable<String,String> defaults,
132                                   int w, int h, Surface window)
133                                   throws SurfaceLost;
134   private native void nativeResize (int w, int h, double rot);
135   private native long nativeRender ();
136   private native void nativeDone ();
137   public native void sendButtonEvent (int x, int y, boolean down);
138   public native void sendMotionEvent (int x, int y);
139   public native void sendKeyEvent (boolean down_p, int code, int mods);
140
141   private void LOG (String fmt, Object... args) {
142     Log.d ("xscreensaver", hack + ": " + String.format (fmt, args));
143   }
144
145   static public String saverNameOf (Object obj) {
146     // Extract the saver name from e.g. "gen.Daydream$BouncingCow"
147     String name = obj.getClass().getSimpleName();
148     int index = name.lastIndexOf('$');
149     if (index != -1) {
150       index++;
151       name = name.substring (index, name.length() - index);
152     }
153     return name.toLowerCase();
154   }
155
156   // Constructor
157   public jwxyz (String hack, Context app, Bitmap screenshot, int w, int h,
158                 Surface surface, Runnable on_quit) {
159
160     this.hack = hack;
161     this.app  = app;
162     this.screenshot = screenshot;
163     this.on_quit = on_quit;
164     this.width = w;
165     this.height = h;
166     this.surface = surface;
167
168     // nativeInit populates 'defaults' with the default values for keys
169     // that are not overridden by SharedPreferences.
170
171     prefs = app.getSharedPreferences (hack, 0);
172
173     // Keep a strong reference to pref_listener, because
174     // registerOnSharedPreferenceChangeListener only uses a weak reference.
175     pref_listener = new PrefListener();
176     prefs.registerOnSharedPreferenceChangeListener (pref_listener);
177
178     scanSystemFonts();
179   }
180
181   protected void finalize() {
182     if (render != null) {
183       LOG ("jwxyz finalized without close. This might be OK.");
184       close();
185     }
186   }
187
188
189   public String getStringResource (String name) {
190
191     name = hack + "_" + name;
192
193     if (prefs.contains(name)) {
194
195       // SharedPreferences is very picky that you request the exact type that
196       // was stored: if it is a float and you ask for a string, you get an
197       // exception instead of the float converted to a string.
198
199       String s = null;
200       try { return prefs.getString (name, "");
201       } catch (Exception e) { }
202
203       try { return Float.toString (prefs.getFloat (name, 0));
204       } catch (Exception e) { }
205
206       try { return Long.toString (prefs.getLong (name, 0));
207       } catch (Exception e) { }
208
209       try { return Integer.toString (prefs.getInt (name, 0));
210       } catch (Exception e) { }
211
212       try { return (prefs.getBoolean (name, false) ? "true" : "false");
213       } catch (Exception e) { }
214     }
215
216     // If we got to here, it's not in there, so return the default.
217     return defaults.get (name);
218   }
219
220
221   private String mungeFontName (String name) {
222     // Roboto-ThinItalic => RobotoThin
223     // AndroidCock Regular => AndroidClock
224     String tails[] = { "Bold", "Italic", "Oblique", "Regular" };
225     for (String tail : tails) {
226       String pres[] = { " ", "-", "_", "" };
227       for (String pre : pres) {
228         int i = name.indexOf(pre + tail);
229         if (i > 0) name = name.substring (0, i);
230       }
231     }
232     return name;
233   }
234
235
236   private void scanSystemFonts() {
237
238     // First parse the system font directories for the global fonts.
239
240     String[] fontdirs = { "/system/fonts", "/system/font", "/data/fonts" };
241     TTFAnalyzer analyzer = new TTFAnalyzer();
242     for (String fontdir : fontdirs) {
243       File dir = new File(fontdir);
244       if (!dir.exists())
245         continue;
246       File[] files = dir.listFiles();
247       if (files == null)
248         continue;
249
250       for (File file : files) {
251         String name = analyzer.getTtfFontName (file.getAbsolutePath());
252         if (name == null) {
253           // LOG ("unparsable system font: %s", file);
254         } else {
255           name = mungeFontName (name);
256           if (! all_fonts.contains (name)) {
257             // LOG ("system font \"%s\" %s", name, file);
258             all_fonts.put (name, name);
259           }
260         }
261       }
262     }
263
264     // Now parse our assets, for our bundled fonts.
265
266     AssetManager am = app.getAssets();
267     String dir = "fonts";
268     String[] files = null;
269     try { files = am.list(dir); }
270     catch (Exception e) { LOG("listing assets: %s", e.toString()); }
271
272     for (String fn : files) {
273       String fn2 = dir + "/" + fn;
274       Typeface t = Typeface.createFromAsset (am, fn2);
275
276       File tmpfile = null;
277       try {
278         tmpfile = new File(app.getCacheDir(), fn);
279         if (tmpfile.createNewFile() == false) {
280           tmpfile.delete();
281           tmpfile.createNewFile();
282         }
283
284         InputStream in = am.open (fn2);
285         FileOutputStream out = new FileOutputStream (tmpfile);
286         byte[] buffer = new byte[1024 * 512];
287         while (in.read(buffer, 0, 1024 * 512) != -1) {
288           out.write(buffer);
289         }
290         out.close();
291         in.close();
292
293         String name = analyzer.getTtfFontName (tmpfile.getAbsolutePath());
294         tmpfile.delete();
295
296         name = mungeFontName (name);
297         all_fonts.put (name, t);
298         // LOG ("asset font \"%s\" %s", name, fn);
299       } catch (Exception e) {
300         if (tmpfile != null) tmpfile.delete();
301         LOG ("error: %s", e.toString());
302       }
303     }
304   }
305
306
307   // Parses family names from X Logical Font Descriptions, including a few
308   // standard X font names that aren't handled by try_xlfd_font().
309   // Returns [ String name, Typeface ]
310   private Object[] parseXLFD (int mask, int traits,
311                               String name, int name_type) {
312     boolean fixed  = false;
313     boolean serif  = false;
314
315     int style_jwxyz = mask & traits;
316
317     if (name_type != FONT_RANDOM) {
318       if ((style_jwxyz & STYLE_BOLD) != 0 ||
319           name.equals("fixed") ||
320           name.equals("courier") ||
321           name.equals("console") ||
322           name.equals("lucidatypewriter") ||
323           name.equals("monospace")) {
324         fixed = true;
325       } else if (name.equals("times") ||
326                  name.equals("georgia") ||
327                  name.equals("serif")) {
328         serif = true;
329       } else if (name.equals("serif-monospace")) {
330         fixed = true;
331         serif = true;
332       }
333     } else {
334       Random r = new Random();
335       serif = r.nextBoolean();      // Not much to randomize here...
336       fixed = (r.nextInt(8) == 0);
337     }
338
339     name = (fixed
340             ? (serif ? "serif-monospace" : "monospace")
341             : (serif ? "serif" : "sans-serif"));
342
343     int style_android = 0;
344     if ((style_jwxyz & STYLE_BOLD) != 0)
345       style_android |= Typeface.BOLD;
346     if ((style_jwxyz & STYLE_ITALIC) != 0)
347       style_android |= Typeface.ITALIC;
348
349     return new Object[] { name, Typeface.create(name, style_android) };
350   }
351
352
353   // Parses "Native Font Name One 12, Native Font Name Two 14".
354   // Returns [ String name, Typeface ]
355   private Object[] parseNativeFont (String name) {
356     Object font2 = all_fonts.get (name);
357     if (font2 instanceof String)
358       font2 = Typeface.create (name, Typeface.NORMAL);
359     return new Object[] { name, (Typeface)font2 };
360   }
361
362
363   // Returns [ Paint paint, String family_name, Float ascent, Float descent ]
364   public Object[] loadFont(int mask, int traits, String name, int name_type,
365                            float size) {
366     Object pair[];
367
368     if (name_type != FONT_RANDOM && name.equals("")) return null;
369
370     if (name_type == FONT_FACE) {
371       pair = parseNativeFont (name);
372     } else {
373       pair = parseXLFD (mask, traits, name, name_type);
374     }
375
376     String name2  = (String)   pair[0];
377     Typeface font = (Typeface) pair[1];
378
379     size *= 2;
380
381     String suffix = (font.isBold() && font.isItalic() ? " bold italic" :
382                      font.isBold()   ? " bold"   :
383                      font.isItalic() ? " italic" :
384                      "");
385     Paint paint = new Paint();
386     paint.setTypeface (font);
387     paint.setTextSize (size);
388     paint.setColor (Color.argb (0xFF, 0xFF, 0xFF, 0xFF));
389
390     LOG ("load font \"%s\" = \"%s %.1f\"", name, name2 + suffix, size);
391
392     FontMetrics fm = paint.getFontMetrics();
393     return new Object[] { paint, name2, -fm.ascent, fm.descent };
394   }
395
396
397   /* Returns a byte[] array containing XCharStruct with an optional
398      bitmap appended to it.
399      lbearing, rbearing, width, ascent, descent: 2 bytes each.
400      Followed by a WxH pixmap, 32 bits per pixel.
401    */
402   public ByteBuffer renderText (Paint paint, String text, boolean render_p,
403                                 boolean antialias_p) {
404
405     if (paint == null) {
406       LOG ("no font");
407       return null;
408     }
409
410     /* Font metric terminology, as used by X11:
411
412        "lbearing" is the distance from the logical origin to the leftmost
413        pixel.  If a character's ink extends to the left of the origin, it is
414        negative.
415
416        "rbearing" is the distance from the logical origin to the rightmost
417        pixel.
418
419        "descent" is the distance from the logical origin to the bottommost
420        pixel.  For characters with descenders, it is positive.  For
421        superscripts, it is negative.
422
423        "ascent" is the distance from the logical origin to the topmost pixel.
424        It is the number of pixels above the baseline.
425
426        "width" is the distance from the logical origin to the position where
427        the logical origin of the next character should be placed.
428
429        If "rbearing" is greater than "width", then this character overlaps the
430        following character.  If smaller, then there is trailing blank space.
431
432        The bbox coordinates returned by getTextBounds grow down and right:
433        for a character with ink both above and below the baseline, top is
434        negative and bottom is positive.
435      */
436     paint.setAntiAlias (antialias_p);
437     FontMetrics fm = paint.getFontMetrics();
438     Rect bbox = new Rect();
439     paint.getTextBounds (text, 0, text.length(), bbox);
440
441     /* The bbox returned by getTextBounds measures from the logical origin
442        with right and down being positive.  This means most characters have
443        a negative top, and characters with descenders have a positive bottom.
444      */
445     int lbearing  =  bbox.left;
446     int rbearing  =  bbox.right;
447     int ascent    = -bbox.top;
448     int descent   =  bbox.bottom;
449     int width     = (int) paint.measureText (text);
450
451     int w = rbearing - lbearing;
452     int h = ascent + descent;
453     int size = 5 * 2 + (render_p ? w * h * 4 : 0);
454
455     ByteBuffer bits = ByteBuffer.allocateDirect (size);
456
457     bits.put ((byte) ((lbearing >> 8) & 0xFF));
458     bits.put ((byte) ( lbearing       & 0xFF));
459     bits.put ((byte) ((rbearing >> 8) & 0xFF));
460     bits.put ((byte) ( rbearing       & 0xFF));
461     bits.put ((byte) ((width    >> 8) & 0xFF));
462     bits.put ((byte) ( width          & 0xFF));
463     bits.put ((byte) ((ascent   >> 8) & 0xFF));
464     bits.put ((byte) ( ascent         & 0xFF));
465     bits.put ((byte) ((descent  >> 8) & 0xFF));
466     bits.put ((byte) ( descent        & 0xFF));
467
468     if (render_p && w > 0 && h > 0) {
469       Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
470       Canvas canvas = new Canvas (bitmap);
471       canvas.drawText (text, -lbearing, ascent, paint);
472       bitmap.copyPixelsToBuffer (bits);
473       bitmap.recycle();
474     }
475
476     return bits;
477   }
478
479
480   /* Returns the contents of the URL.
481      Loads the URL in a background thread: if the URL has not yet loaded,
482      this will return null.  Once the URL has completely loaded, the full
483      contents will be returned.  Calling this again after that starts the
484      URL loading again.
485    */
486   private String loading_url = null;
487   private ByteBuffer loaded_url_body = null;
488
489   public synchronized ByteBuffer loadURL (String url) {
490
491     if (loaded_url_body != null) {                      // Thread finished
492
493       // LOG ("textclient finished %s", loading_url);
494
495       ByteBuffer bb = loaded_url_body;
496       loading_url = null;
497       loaded_url_body = null;
498       return bb;
499
500     } else if (loading_url != null) {                   // Waiting on thread
501       // LOG ("textclient waiting...");
502       return null;
503
504     } else {                                            // Launch thread
505
506       loading_url = url;
507       LOG ("textclient launching %s...", url);
508
509       new Thread (new Runnable() {
510           public void run() {
511             int size0 = 10240;
512             int size = size0;
513             int count = 0;
514             ByteBuffer body = ByteBuffer.allocateDirect (size);
515
516             try {
517               URL u = new URL (loading_url);
518               // LOG ("textclient thread loading: %s", u.toString());
519               InputStream s = u.openStream();
520               byte buf[] = new byte[10240];
521               while (true) {
522                 int n = s.read (buf);
523                 if (n == -1) break;
524                 // LOG ("textclient thread read %d", n);
525                 if (count + n + 1 >= size) {
526                   int size2 = (int) (size * 1.2 + size0);
527                   // LOG ("textclient thread expand %d -> %d", size, size2);
528                   ByteBuffer body2 = ByteBuffer.allocateDirect (size2);
529                   body.rewind();
530                   body2.put (body);
531                   body2.position (count);
532                   body = body2;
533                   size = size2;
534                 }
535                 body.put (buf, 0, n);
536                 count += n;
537               }
538             } catch (Exception e) {
539               LOG ("load URL error: %s", e.toString());
540               body.clear();
541               body.put (e.toString().getBytes());
542               body.put ((byte) 0);
543             }
544
545             // LOG ("textclient thread finished %s (%d)", loading_url, size);
546             loaded_url_body = body;
547           }
548         }).start();
549
550       return null;
551     }
552   }
553
554
555   private ByteBuffer convertBitmap (String name, Bitmap bitmap,
556                                     int target_width, int target_height,
557                                     ExifInterface exif,
558                                     boolean rotate_p) {
559     if (bitmap == null) return null;
560
561     try {
562
563       int width  = bitmap.getWidth();
564       int height = bitmap.getHeight();
565
566       LOG ("read image %s: %d x %d", name, width, height);
567
568       // First rotate the image as per EXIF.
569
570       if (exif != null) {
571         int deg = 0;
572         switch (exif.getAttributeInt (ExifInterface.TAG_ORIENTATION,
573                                       ExifInterface.ORIENTATION_NORMAL)) {
574         case ExifInterface.ORIENTATION_ROTATE_90:  deg = 90;  break;
575         case ExifInterface.ORIENTATION_ROTATE_180: deg = 180; break;
576         case ExifInterface.ORIENTATION_ROTATE_270: deg = 270; break;
577         }
578         if (deg != 0) {
579           LOG ("%s: EXIF rotate %d", name, deg);
580           Matrix matrix = new Matrix();
581           matrix.preRotate (deg);
582           bitmap = Bitmap.createBitmap (bitmap, 0, 0, width, height,
583                                         matrix, true);
584           width  = bitmap.getWidth();
585           height = bitmap.getHeight();
586         }
587       }
588
589       // If the caller requested that we rotate the image to best fit the
590       // screen, rotate it again.  (Could combine this with the above and
591       // avoid copying the image again, but I'm lazy.)
592
593       if (rotate_p &&
594           (width > height) != (target_width > target_height)) {
595         LOG ("%s: rotated to fit screen", name);
596         Matrix matrix = new Matrix();
597         matrix.preRotate (90);
598         bitmap = Bitmap.createBitmap (bitmap, 0, 0, width, height,
599                                       matrix, true);
600         width  = bitmap.getWidth();
601         height = bitmap.getHeight();
602       }
603
604       // Resize the image to be not larger than the screen, potentially
605       // copying it for the third time.
606       // Actually, always scale it, scaling up if necessary.
607
608 //    if (width > target_width || height > target_height)
609       {
610         float r1 = target_width  / (float) width;
611         float r2 = target_height / (float) height;
612         float r = (r1 > r2 ? r2 : r1);
613         LOG ("%s: resize %.1f: %d x %d => %d x %d", name,
614              r, width, height, (int) (width * r), (int) (height * r));
615         width  = (int) (width * r);
616         height = (int) (height * r);
617         bitmap = Bitmap.createScaledBitmap (bitmap, width, height, true);
618         width  = bitmap.getWidth();
619         height = bitmap.getHeight();
620       }
621
622       // Now convert it to a ByteBuffer in the form expected by the C caller.
623
624       byte[] nameb = name.getBytes("UTF-8");
625       int size     = bitmap.getByteCount() + 4 + nameb.length + 1;
626
627       ByteBuffer bits = ByteBuffer.allocateDirect (size);
628
629       bits.put ((byte) ((width  >> 8) & 0xFF));
630       bits.put ((byte) ( width        & 0xFF));
631       bits.put ((byte) ((height >> 8) & 0xFF));
632       bits.put ((byte) ( height       & 0xFF));
633       bits.put (nameb);
634       bits.put ((byte) 0);
635
636       // The fourth of five copies.  Good thing these are supercomputers.
637       bitmap.copyPixelsToBuffer (bits);
638
639       return bits;
640
641     } catch (Exception e) {
642       LOG ("image %s unreadable: %s", name, e.toString());
643     }
644
645     return null;
646   }
647
648
649   public ByteBuffer loadRandomImage (int target_width, int target_height,
650                                      boolean rotate_p) {
651
652     int min_size = 480;
653     int max_size = 0x7FFF;
654
655     ArrayList<String> imgs = new ArrayList<String>();
656
657     ContentResolver cr = app.getContentResolver();
658     String[] cols = { MediaColumns.DATA,
659                       MediaColumns.MIME_TYPE,
660                       MediaColumns.WIDTH,
661                       MediaColumns.HEIGHT };
662     Uri uris[] = {
663       android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI,
664       android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI };
665
666     for (int i = 0; i < uris.length; i++) {
667       Cursor cursor = cr.query (uris[i], cols, null, null, null);
668       int j = 0;
669       int path_col   = cursor.getColumnIndexOrThrow (cols[j++]);
670       int type_col   = cursor.getColumnIndexOrThrow (cols[j++]);
671       int width_col  = cursor.getColumnIndexOrThrow (cols[j++]);
672       int height_col = cursor.getColumnIndexOrThrow (cols[j++]);
673       while (cursor.moveToNext()) {
674         String path = cursor.getString(path_col);
675         String type = cursor.getString(type_col);
676         int w = Integer.parseInt (cursor.getString(width_col));
677         int h = Integer.parseInt (cursor.getString(height_col));
678         if (type.startsWith("image/") &&
679             w > min_size && h > min_size &&
680             w < max_size && h < max_size) {
681           imgs.add (path);
682         }
683       }
684       cursor.close();
685     }
686
687     String which = null;
688
689     int count = imgs.size();
690     if (count == 0) {
691       LOG ("no images");
692       return null;
693     }
694
695     int i = new Random().nextInt (count);
696     which = imgs.get (i);
697     LOG ("picked image %d of %d: %s", i, count, which);
698
699     Uri uri = Uri.fromFile (new File (which));
700     String name = uri.getLastPathSegment();
701     Bitmap bitmap = null;
702     ExifInterface exif = null;
703
704     try {
705       bitmap = MediaStore.Images.Media.getBitmap (cr, uri);
706     } catch (Exception e) {
707       LOG ("image %s unloadable: %s", which, e.toString());
708       return null;
709     }
710
711     try {
712       exif = new ExifInterface (uri.getPath());  // If it fails, who cares
713     } catch (Exception e) {
714     }
715
716     ByteBuffer bits = convertBitmap (name, bitmap,
717                                      target_width, target_height,
718                                      exif, rotate_p);
719     bitmap.recycle();
720     return bits;
721   }
722
723
724   public ByteBuffer getScreenshot (int target_width, int target_height,
725                                    boolean rotate_p) {
726     return convertBitmap ("Screenshot", screenshot,
727                           target_width, target_height,
728                           null, rotate_p);
729   }
730
731
732   // Sadly duplicated from jwxyz.h (and thence X.h and keysymdef.h)
733   //
734   private static final int ShiftMask =     (1<<0);
735   private static final int LockMask =      (1<<1);
736   private static final int ControlMask =   (1<<2);
737   private static final int Mod1Mask =      (1<<3);
738   private static final int Mod2Mask =      (1<<4);
739   private static final int Mod3Mask =      (1<<5);
740   private static final int Mod4Mask =      (1<<6);
741   private static final int Mod5Mask =      (1<<7);
742   private static final int Button1Mask =   (1<<8);
743   private static final int Button2Mask =   (1<<9);
744   private static final int Button3Mask =   (1<<10);
745   private static final int Button4Mask =   (1<<11);
746   private static final int Button5Mask =   (1<<12);
747
748   private static final int XK_Shift_L =    0xFFE1;
749   private static final int XK_Shift_R =    0xFFE2;
750   private static final int XK_Control_L =  0xFFE3;
751   private static final int XK_Control_R =  0xFFE4;
752   private static final int XK_Caps_Lock =  0xFFE5;
753   private static final int XK_Shift_Lock = 0xFFE6;
754   private static final int XK_Meta_L =     0xFFE7;
755   private static final int XK_Meta_R =     0xFFE8;
756   private static final int XK_Alt_L =      0xFFE9;
757   private static final int XK_Alt_R =      0xFFEA;
758   private static final int XK_Super_L =    0xFFEB;
759   private static final int XK_Super_R =    0xFFEC;
760   private static final int XK_Hyper_L =    0xFFED;
761   private static final int XK_Hyper_R =    0xFFEE;
762
763   private static final int XK_Home =       0xFF50;
764   private static final int XK_Left =       0xFF51;
765   private static final int XK_Up =         0xFF52;
766   private static final int XK_Right =      0xFF53;
767   private static final int XK_Down =       0xFF54;
768   private static final int XK_Prior =      0xFF55;
769   private static final int XK_Page_Up =    0xFF55;
770   private static final int XK_Next =       0xFF56;
771   private static final int XK_Page_Down =  0xFF56;
772   private static final int XK_End =        0xFF57;
773   private static final int XK_Begin =      0xFF58;
774
775   private static final int XK_F1 =         0xFFBE;
776   private static final int XK_F2 =         0xFFBF;
777   private static final int XK_F3 =         0xFFC0;
778   private static final int XK_F4 =         0xFFC1;
779   private static final int XK_F5 =         0xFFC2;
780   private static final int XK_F6 =         0xFFC3;
781   private static final int XK_F7 =         0xFFC4;
782   private static final int XK_F8 =         0xFFC5;
783   private static final int XK_F9 =         0xFFC6;
784   private static final int XK_F10 =        0xFFC7;
785   private static final int XK_F11 =        0xFFC8;
786   private static final int XK_F12 =        0xFFC9;
787
788   public void sendKeyEvent (KeyEvent event) {
789     int uc    = event.getUnicodeChar();
790     int jcode = event.getKeyCode();
791     int jmods = event.getModifiers();
792     int xcode = 0;
793     int xmods = 0;
794
795     switch (jcode) {
796     case KeyEvent.KEYCODE_SHIFT_LEFT:        xcode = XK_Shift_L;   break;
797     case KeyEvent.KEYCODE_SHIFT_RIGHT:       xcode = XK_Shift_R;   break;
798     case KeyEvent.KEYCODE_CTRL_LEFT:         xcode = XK_Control_L; break;
799     case KeyEvent.KEYCODE_CTRL_RIGHT:        xcode = XK_Control_R; break;
800     case KeyEvent.KEYCODE_CAPS_LOCK:         xcode = XK_Caps_Lock; break;
801     case KeyEvent.KEYCODE_META_LEFT:         xcode = XK_Meta_L;    break;
802     case KeyEvent.KEYCODE_META_RIGHT:        xcode = XK_Meta_R;    break;
803     case KeyEvent.KEYCODE_ALT_LEFT:          xcode = XK_Alt_L;     break;
804     case KeyEvent.KEYCODE_ALT_RIGHT:         xcode = XK_Alt_R;     break;
805
806     case KeyEvent.KEYCODE_HOME:              xcode = XK_Home;      break;
807     case KeyEvent.KEYCODE_DPAD_LEFT:         xcode = XK_Left;      break;
808     case KeyEvent.KEYCODE_DPAD_UP:           xcode = XK_Up;        break;
809     case KeyEvent.KEYCODE_DPAD_RIGHT:        xcode = XK_Right;     break;
810     case KeyEvent.KEYCODE_DPAD_DOWN:         xcode = XK_Down;      break;
811   //case KeyEvent.KEYCODE_NAVIGATE_PREVIOUS: xcode = XK_Prior;     break;
812     case KeyEvent.KEYCODE_PAGE_UP:           xcode = XK_Page_Up;   break;
813   //case KeyEvent.KEYCODE_NAVIGATE_NEXT:     xcode = XK_Next;      break;
814     case KeyEvent.KEYCODE_PAGE_DOWN:         xcode = XK_Page_Down; break;
815     case KeyEvent.KEYCODE_MOVE_END:          xcode = XK_End;       break;
816     case KeyEvent.KEYCODE_MOVE_HOME:         xcode = XK_Begin;     break;
817
818     case KeyEvent.KEYCODE_F1:                xcode = XK_F1;        break;
819     case KeyEvent.KEYCODE_F2:                xcode = XK_F2;        break;
820     case KeyEvent.KEYCODE_F3:                xcode = XK_F3;        break;
821     case KeyEvent.KEYCODE_F4:                xcode = XK_F4;        break;
822     case KeyEvent.KEYCODE_F5:                xcode = XK_F5;        break;
823     case KeyEvent.KEYCODE_F6:                xcode = XK_F6;        break;
824     case KeyEvent.KEYCODE_F7:                xcode = XK_F7;        break;
825     case KeyEvent.KEYCODE_F8:                xcode = XK_F8;        break;
826     case KeyEvent.KEYCODE_F9:                xcode = XK_F9;        break;
827     case KeyEvent.KEYCODE_F10:               xcode = XK_F10;       break;
828     case KeyEvent.KEYCODE_F11:               xcode = XK_F11;       break;
829     case KeyEvent.KEYCODE_F12:               xcode = XK_F12;       break;
830     default:                                 xcode = uc;           break;
831     }
832
833     if (0 != (jmods & KeyEvent.META_SHIFT_ON))     xmods |= ShiftMask;
834     if (0 != (jmods & KeyEvent.META_CAPS_LOCK_ON)) xmods |= LockMask;
835     if (0 != (jmods & KeyEvent.META_CTRL_MASK))    xmods |= ControlMask;
836     if (0 != (jmods & KeyEvent.META_ALT_MASK))     xmods |= Mod1Mask;
837     if (0 != (jmods & KeyEvent.META_META_ON))      xmods |= Mod1Mask;
838     if (0 != (jmods & KeyEvent.META_SYM_ON))       xmods |= Mod2Mask;
839     if (0 != (jmods & KeyEvent.META_FUNCTION_ON))  xmods |= Mod3Mask;
840
841     /* If you touch and release Shift, you get no events.
842        If you type Shift-A, you get Shift down, A down, A up, Shift up.
843        So let's just ignore all lone modifier key events.
844      */
845     if (xcode >= XK_Shift_L && xcode <= XK_Hyper_R)
846       return;
847
848     boolean down_p = event.getAction() == KeyEvent.ACTION_DOWN;
849     sendKeyEvent (down_p, xcode, xmods);
850   }
851
852   void start () {
853     if (render == null) {
854       animating_p = true;
855       render = new Thread(new Runnable() {
856         @Override
857         public void run()
858         {
859           int currentWidth, currentHeight;
860           synchronized (render) {
861             while (true) {
862               while (!animating_p || width == 0 || height == 0) {
863                 try {
864                   render.wait();
865                 } catch(InterruptedException exc) {
866                   return;
867                 }
868               }
869
870               try {
871                 nativeInit (hack, defaults, width, height, surface);
872                 currentWidth = width;
873                 currentHeight= height;
874                 break;
875               } catch (SurfaceLost exc) {
876                 width = 0;
877                 height = 0;
878               }
879             }
880           }
881
882         main_loop:
883           while (true) {
884             synchronized (render) {
885               assert width != 0;
886               assert height != 0;
887               while (!animating_p) {
888                 try {
889                   render.wait();
890                 } catch(InterruptedException exc) {
891                   break main_loop;
892                 }
893               }
894
895               if (currentWidth != width || currentHeight != height) {
896                 currentWidth = width;
897                 currentHeight = height;
898                 nativeResize (width, height, 0);
899               }
900             }
901
902             long delay = nativeRender();
903
904             synchronized (render) {
905               if (delay != 0) {
906                 try {
907                   render.wait(delay / 1000, (int)(delay % 1000) * 1000);
908                 } catch (InterruptedException exc) {
909                   break main_loop;
910                 }
911               } else {
912                 if (Thread.interrupted ()) {
913                   break main_loop;
914                 }
915               }
916             }
917           }
918
919           assert nativeRunningHackPtr != 0;
920           nativeDone ();
921         }
922       });
923
924       render.start();
925     } else {
926       synchronized(render) {
927         animating_p = true;
928         render.notify();
929       }
930     }
931   }
932
933   void pause () {
934     if (render == null)
935       return;
936     synchronized (render) {
937       animating_p = false;
938       render.notify();
939     }
940   }
941
942   void close () {
943     if (render == null)
944       return;
945     synchronized (render) {
946       animating_p = false;
947       render.interrupt();
948     }
949     try {
950       render.join();
951     } catch (InterruptedException exc) {
952     }
953     render = null;
954   }
955
956   void resize (int w, int h) {
957     assert w != 0;
958     assert h != 0;
959     if (render != null) {
960       synchronized (render) {
961         width = w;
962         height = h;
963         render.notify();
964       }
965     } else {
966       width = w;
967       height = h;
968     }
969   }
970
971
972   /* We distinguish between taps and drags.
973
974      - Drags/pans (down, motion, up) are sent to the saver to handle.
975      - Single-taps exit the saver.
976      - Long-press single-taps are sent to the saver as ButtonPress/Release;
977      - Double-taps are sent to the saver as a "Space" keypress.
978
979      #### TODO:
980      - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow.
981    */
982
983   @Override
984   public boolean onSingleTapConfirmed (MotionEvent event) {
985     if (on_quit != null)
986       on_quit.run();
987     return true;
988   }
989
990   @Override
991   public boolean onDoubleTap (MotionEvent event) {
992     sendKeyEvent (new KeyEvent (KeyEvent.ACTION_DOWN,
993                                 KeyEvent.KEYCODE_SPACE));
994     return true;
995   }
996
997   @Override
998   public void onLongPress (MotionEvent event) {
999     if (! button_down_p) {
1000       int x = (int) event.getX (event.getPointerId (0));
1001       int y = (int) event.getY (event.getPointerId (0));
1002       sendButtonEvent (x, y, true);
1003       sendButtonEvent (x, y, false);
1004     }
1005   }
1006
1007   @Override
1008   public void onShowPress (MotionEvent event) {
1009     if (! button_down_p) {
1010       button_down_p = true;
1011       int x = (int) event.getX (event.getPointerId (0));
1012       int y = (int) event.getY (event.getPointerId (0));
1013       sendButtonEvent (x, y, true);
1014     }
1015   }
1016
1017   @Override
1018   public boolean onScroll (MotionEvent e1, MotionEvent e2, 
1019                            float distanceX, float distanceY) {
1020     // LOG ("onScroll: %d", button_down_p ? 1 : 0);
1021     if (button_down_p)
1022       sendMotionEvent ((int) e2.getX (e2.getPointerId (0)),
1023                        (int) e2.getY (e2.getPointerId (0)));
1024     return true;
1025   }
1026
1027   // If you drag too fast, you get a single onFling event instead of a
1028   // succession of onScroll events.  I can't figure out how to disable it.
1029   @Override
1030   public boolean onFling (MotionEvent e1, MotionEvent e2, 
1031                           float velocityX, float velocityY) {
1032     return false;
1033   }
1034
1035   public boolean dragEnded (MotionEvent event) {
1036     if (button_down_p) {
1037       int x = (int) event.getX (event.getPointerId (0));
1038       int y = (int) event.getY (event.getPointerId (0));
1039       sendButtonEvent (x, y, false);
1040       button_down_p = false;
1041     }
1042     return true;
1043   }
1044
1045   @Override
1046   public boolean onDown (MotionEvent event) {
1047     return false;
1048   }
1049
1050   @Override
1051   public boolean onSingleTapUp (MotionEvent event) {
1052     return false;
1053   }
1054
1055   @Override
1056   public boolean onDoubleTapEvent (MotionEvent event) {
1057     return false;
1058   }
1059
1060
1061   static {
1062     System.loadLibrary ("xscreensaver");
1063
1064 /*
1065     Thread.setDefaultUncaughtExceptionHandler(
1066       new Thread.UncaughtExceptionHandler() {
1067         Thread.UncaughtExceptionHandler old_handler =
1068           Thread.currentThread().getUncaughtExceptionHandler();
1069
1070         @Override
1071         public void uncaughtException (Thread thread, Throwable ex) {
1072           String err = ex.toString();
1073           Log.d ("xscreensaver", "Caught exception: " + err);
1074           old_handler.uncaughtException (thread, ex);
1075         }
1076       });
1077 */
1078   }
1079 }