8ca6af40588b0fcbb8e8affa8d826f48aa0036d8
[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-2018 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.BitmapFactory;
37 import android.graphics.Canvas;
38 import android.graphics.Color;
39 import android.graphics.Matrix;
40 import android.net.Uri;
41 import android.view.GestureDetector;
42 import android.view.KeyEvent;
43 import android.view.MotionEvent;
44 import java.net.URL;
45 import java.nio.ByteBuffer;
46 import java.io.File;
47 import java.io.InputStream;
48 import java.io.FileOutputStream;
49 import java.lang.InterruptedException;
50 import java.lang.Runnable;
51 import java.lang.Thread;
52 import java.util.TimerTask;
53 import android.database.Cursor;
54 import android.provider.MediaStore;
55 import android.provider.MediaStore.MediaColumns;
56 import android.media.ExifInterface;
57 import org.jwz.xscreensaver.TTFAnalyzer;
58 import android.util.Log;
59 import android.view.Surface;
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   // Returns [ Bitmap bitmap, String name ]
556   private Object[] convertBitmap (String name, Bitmap bitmap,
557                                   int target_width, int target_height,
558                                   ExifInterface exif, boolean rotate_p) {
559     if (bitmap == null) return null;
560
561     {
562
563       int width  = bitmap.getWidth();
564       int height = bitmap.getHeight();
565       Matrix matrix = new Matrix();
566
567       LOG ("read image %s: %d x %d", name, width, height);
568
569       // First rotate the image as per EXIF.
570
571       if (exif != null) {
572         int deg = 0;
573         switch (exif.getAttributeInt (ExifInterface.TAG_ORIENTATION,
574                                       ExifInterface.ORIENTATION_NORMAL)) {
575         case ExifInterface.ORIENTATION_ROTATE_90:  deg = 90;  break;
576         case ExifInterface.ORIENTATION_ROTATE_180: deg = 180; break;
577         case ExifInterface.ORIENTATION_ROTATE_270: deg = 270; break;
578         }
579         if (deg != 0) {
580           LOG ("%s: EXIF rotate %d", name, deg);
581           matrix.preRotate (deg);
582           if (deg == 90 || deg == 270) {
583             int temp = width;
584             width = height;
585             height = temp;
586           }
587         }
588       }
589
590       // If the caller requested that we rotate the image to best fit the
591       // screen, rotate it again.
592
593       if (rotate_p &&
594           (width > height) != (target_width > target_height)) {
595         LOG ("%s: rotated to fit screen", name);
596         matrix.preRotate (90);
597
598         int temp = width;
599         width = height;
600         height = temp;
601       }
602
603       // Resize the image to be not larger than the screen, potentially
604       // copying it for the third time.
605       // Actually, always scale it, scaling up if necessary.
606
607 //    if (width > target_width || height > target_height)
608       {
609         float r1 = target_width  / (float) width;
610         float r2 = target_height / (float) height;
611         float r = (r1 > r2 ? r2 : r1);
612         LOG ("%s: resize %.1f: %d x %d => %d x %d", name,
613              r, width, height, (int) (width * r), (int) (height * r));
614         matrix.preScale (r, r);
615       }
616
617       bitmap =  Bitmap.createBitmap (bitmap, 0, 0,
618                                      bitmap.getWidth(), bitmap.getHeight(),
619                                      matrix, true);
620
621       if (bitmap.getConfig() != Bitmap.Config.ARGB_8888)
622         bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false);
623
624       return new Object[] { bitmap, name };
625
626     }
627   }
628
629
630   public Object[] loadRandomImage (int target_width, int target_height,
631                                    boolean rotate_p) {
632
633     int min_size = 480;
634     int max_size = 0x7FFF;
635
636     ArrayList<String> imgs = new ArrayList<String>();
637
638     ContentResolver cr = app.getContentResolver();
639     String[] cols = { MediaColumns.DATA,
640                       MediaColumns.MIME_TYPE,
641                       MediaColumns.WIDTH,
642                       MediaColumns.HEIGHT };
643     Uri uris[] = {
644       android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI,
645       android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI };
646
647     for (int i = 0; i < uris.length; i++) {
648       Cursor cursor = cr.query (uris[i], cols, null, null, null);
649       if (cursor == null)
650         continue;
651       int j = 0;
652       int path_col   = cursor.getColumnIndexOrThrow (cols[j++]);
653       int type_col   = cursor.getColumnIndexOrThrow (cols[j++]);
654       int width_col  = cursor.getColumnIndexOrThrow (cols[j++]);
655       int height_col = cursor.getColumnIndexOrThrow (cols[j++]);
656       while (cursor.moveToNext()) {
657         String path = cursor.getString(path_col);
658         String type = cursor.getString(type_col);
659         if (path != null && type != null && type.startsWith("image/")) {
660           String wc = cursor.getString(width_col);
661           String hc = cursor.getString(height_col);
662           if (wc != null && hc != null) {
663             int w = Integer.parseInt (wc);
664             int h = Integer.parseInt (hc);
665             if (w > min_size && h > min_size &&
666                 w < max_size && h < max_size) {
667               imgs.add (path);
668             }
669           }
670         }
671       }
672       cursor.close();
673     }
674
675     String which = null;
676
677     int count = imgs.size();
678     if (count == 0) {
679       LOG ("no images");
680       return null;
681     }
682
683     int i = new Random().nextInt (count);
684     which = imgs.get (i);
685     LOG ("picked image %d of %d: %s", i, count, which);
686
687     Uri uri = Uri.fromFile (new File (which));
688     String name = uri.getLastPathSegment();
689     Bitmap bitmap = null;
690     ExifInterface exif = null;
691
692     try {
693       try {
694         bitmap = MediaStore.Images.Media.getBitmap (cr, uri);
695       } catch (Exception e) {
696         LOG ("image %s unloadable: %s", which, e.toString());
697         return null;
698       }
699
700       try {
701         exif = new ExifInterface (uri.getPath());  // If it fails, who cares
702       } catch (Exception e) {
703       }
704
705       return convertBitmap (name, bitmap, target_width, target_height,
706                             exif, rotate_p);
707     } catch (java.lang.OutOfMemoryError e) {
708       LOG ("image %s got OutOfMemoryError: %s", which, e.toString());
709       return null;
710     }
711   }
712
713
714   public Object[] getScreenshot (int target_width, int target_height,
715                                boolean rotate_p) {
716     return convertBitmap ("Screenshot", screenshot,
717                           target_width, target_height,
718                           null, rotate_p);
719   }
720
721
722   public Bitmap decodePNG (byte[] data) {
723     BitmapFactory.Options opts = new BitmapFactory.Options();
724     opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
725     return BitmapFactory.decodeByteArray (data, 0, data.length, opts);
726   }
727
728
729   // Sadly duplicated from jwxyz.h (and thence X.h and keysymdef.h)
730   //
731   private static final int ShiftMask =     (1<<0);
732   private static final int LockMask =      (1<<1);
733   private static final int ControlMask =   (1<<2);
734   private static final int Mod1Mask =      (1<<3);
735   private static final int Mod2Mask =      (1<<4);
736   private static final int Mod3Mask =      (1<<5);
737   private static final int Mod4Mask =      (1<<6);
738   private static final int Mod5Mask =      (1<<7);
739   private static final int Button1Mask =   (1<<8);
740   private static final int Button2Mask =   (1<<9);
741   private static final int Button3Mask =   (1<<10);
742   private static final int Button4Mask =   (1<<11);
743   private static final int Button5Mask =   (1<<12);
744
745   private static final int XK_Shift_L =    0xFFE1;
746   private static final int XK_Shift_R =    0xFFE2;
747   private static final int XK_Control_L =  0xFFE3;
748   private static final int XK_Control_R =  0xFFE4;
749   private static final int XK_Caps_Lock =  0xFFE5;
750   private static final int XK_Shift_Lock = 0xFFE6;
751   private static final int XK_Meta_L =     0xFFE7;
752   private static final int XK_Meta_R =     0xFFE8;
753   private static final int XK_Alt_L =      0xFFE9;
754   private static final int XK_Alt_R =      0xFFEA;
755   private static final int XK_Super_L =    0xFFEB;
756   private static final int XK_Super_R =    0xFFEC;
757   private static final int XK_Hyper_L =    0xFFED;
758   private static final int XK_Hyper_R =    0xFFEE;
759
760   private static final int XK_Home =       0xFF50;
761   private static final int XK_Left =       0xFF51;
762   private static final int XK_Up =         0xFF52;
763   private static final int XK_Right =      0xFF53;
764   private static final int XK_Down =       0xFF54;
765   private static final int XK_Prior =      0xFF55;
766   private static final int XK_Page_Up =    0xFF55;
767   private static final int XK_Next =       0xFF56;
768   private static final int XK_Page_Down =  0xFF56;
769   private static final int XK_End =        0xFF57;
770   private static final int XK_Begin =      0xFF58;
771
772   private static final int XK_F1 =         0xFFBE;
773   private static final int XK_F2 =         0xFFBF;
774   private static final int XK_F3 =         0xFFC0;
775   private static final int XK_F4 =         0xFFC1;
776   private static final int XK_F5 =         0xFFC2;
777   private static final int XK_F6 =         0xFFC3;
778   private static final int XK_F7 =         0xFFC4;
779   private static final int XK_F8 =         0xFFC5;
780   private static final int XK_F9 =         0xFFC6;
781   private static final int XK_F10 =        0xFFC7;
782   private static final int XK_F11 =        0xFFC8;
783   private static final int XK_F12 =        0xFFC9;
784
785   public void sendKeyEvent (KeyEvent event) {
786     int uc    = event.getUnicodeChar();
787     int jcode = event.getKeyCode();
788     int jmods = event.getModifiers();
789     int xcode = 0;
790     int xmods = 0;
791
792     switch (jcode) {
793     case KeyEvent.KEYCODE_SHIFT_LEFT:        xcode = XK_Shift_L;   break;
794     case KeyEvent.KEYCODE_SHIFT_RIGHT:       xcode = XK_Shift_R;   break;
795     case KeyEvent.KEYCODE_CTRL_LEFT:         xcode = XK_Control_L; break;
796     case KeyEvent.KEYCODE_CTRL_RIGHT:        xcode = XK_Control_R; break;
797     case KeyEvent.KEYCODE_CAPS_LOCK:         xcode = XK_Caps_Lock; break;
798     case KeyEvent.KEYCODE_META_LEFT:         xcode = XK_Meta_L;    break;
799     case KeyEvent.KEYCODE_META_RIGHT:        xcode = XK_Meta_R;    break;
800     case KeyEvent.KEYCODE_ALT_LEFT:          xcode = XK_Alt_L;     break;
801     case KeyEvent.KEYCODE_ALT_RIGHT:         xcode = XK_Alt_R;     break;
802
803     case KeyEvent.KEYCODE_HOME:              xcode = XK_Home;      break;
804     case KeyEvent.KEYCODE_DPAD_LEFT:         xcode = XK_Left;      break;
805     case KeyEvent.KEYCODE_DPAD_UP:           xcode = XK_Up;        break;
806     case KeyEvent.KEYCODE_DPAD_RIGHT:        xcode = XK_Right;     break;
807     case KeyEvent.KEYCODE_DPAD_DOWN:         xcode = XK_Down;      break;
808   //case KeyEvent.KEYCODE_NAVIGATE_PREVIOUS: xcode = XK_Prior;     break;
809     case KeyEvent.KEYCODE_PAGE_UP:           xcode = XK_Page_Up;   break;
810   //case KeyEvent.KEYCODE_NAVIGATE_NEXT:     xcode = XK_Next;      break;
811     case KeyEvent.KEYCODE_PAGE_DOWN:         xcode = XK_Page_Down; break;
812     case KeyEvent.KEYCODE_MOVE_END:          xcode = XK_End;       break;
813     case KeyEvent.KEYCODE_MOVE_HOME:         xcode = XK_Begin;     break;
814
815     case KeyEvent.KEYCODE_F1:                xcode = XK_F1;        break;
816     case KeyEvent.KEYCODE_F2:                xcode = XK_F2;        break;
817     case KeyEvent.KEYCODE_F3:                xcode = XK_F3;        break;
818     case KeyEvent.KEYCODE_F4:                xcode = XK_F4;        break;
819     case KeyEvent.KEYCODE_F5:                xcode = XK_F5;        break;
820     case KeyEvent.KEYCODE_F6:                xcode = XK_F6;        break;
821     case KeyEvent.KEYCODE_F7:                xcode = XK_F7;        break;
822     case KeyEvent.KEYCODE_F8:                xcode = XK_F8;        break;
823     case KeyEvent.KEYCODE_F9:                xcode = XK_F9;        break;
824     case KeyEvent.KEYCODE_F10:               xcode = XK_F10;       break;
825     case KeyEvent.KEYCODE_F11:               xcode = XK_F11;       break;
826     case KeyEvent.KEYCODE_F12:               xcode = XK_F12;       break;
827     default:                                 xcode = uc;           break;
828     }
829
830     if (0 != (jmods & KeyEvent.META_SHIFT_ON))     xmods |= ShiftMask;
831     if (0 != (jmods & KeyEvent.META_CAPS_LOCK_ON)) xmods |= LockMask;
832     if (0 != (jmods & KeyEvent.META_CTRL_MASK))    xmods |= ControlMask;
833     if (0 != (jmods & KeyEvent.META_ALT_MASK))     xmods |= Mod1Mask;
834     if (0 != (jmods & KeyEvent.META_META_ON))      xmods |= Mod1Mask;
835     if (0 != (jmods & KeyEvent.META_SYM_ON))       xmods |= Mod2Mask;
836     if (0 != (jmods & KeyEvent.META_FUNCTION_ON))  xmods |= Mod3Mask;
837
838     /* If you touch and release Shift, you get no events.
839        If you type Shift-A, you get Shift down, A down, A up, Shift up.
840        So let's just ignore all lone modifier key events.
841      */
842     if (xcode >= XK_Shift_L && xcode <= XK_Hyper_R)
843       return;
844
845     boolean down_p = event.getAction() == KeyEvent.ACTION_DOWN;
846     sendKeyEvent (down_p, xcode, xmods);
847   }
848
849   void start () {
850     if (render == null) {
851       animating_p = true;
852       render = new Thread(new Runnable() {
853         @Override
854         public void run()
855         {
856           int currentWidth, currentHeight;
857           synchronized (render) {
858             while (true) {
859               while (!animating_p || width == 0 || height == 0) {
860                 try {
861                   render.wait();
862                 } catch(InterruptedException exc) {
863                   return;
864                 }
865               }
866
867               try {
868                 nativeInit (hack, defaults, width, height, surface);
869                 currentWidth = width;
870                 currentHeight= height;
871                 break;
872               } catch (SurfaceLost exc) {
873                 width = 0;
874                 height = 0;
875               }
876             }
877           }
878
879         main_loop:
880           while (true) {
881             synchronized (render) {
882               assert width != 0;
883               assert height != 0;
884               while (!animating_p) {
885                 try {
886                   render.wait();
887                 } catch(InterruptedException exc) {
888                   break main_loop;
889                 }
890               }
891
892               if (currentWidth != width || currentHeight != height) {
893                 currentWidth = width;
894                 currentHeight = height;
895                 nativeResize (width, height, 0);
896               }
897             }
898
899             long delay = nativeRender();
900
901             synchronized (render) {
902               if (delay != 0) {
903                 try {
904                   render.wait(delay / 1000, (int)(delay % 1000) * 1000);
905                 } catch (InterruptedException exc) {
906                   break main_loop;
907                 }
908               } else {
909                 if (Thread.interrupted ()) {
910                   break main_loop;
911                 }
912               }
913             }
914           }
915
916           assert nativeRunningHackPtr != 0;
917           nativeDone ();
918         }
919       });
920
921       render.start();
922     } else {
923       synchronized(render) {
924         animating_p = true;
925         render.notify();
926       }
927     }
928   }
929
930   void pause () {
931     if (render == null)
932       return;
933     synchronized (render) {
934       animating_p = false;
935       render.notify();
936     }
937   }
938
939   void close () {
940     if (render == null)
941       return;
942     synchronized (render) {
943       animating_p = false;
944       render.interrupt();
945     }
946     try {
947       render.join();
948     } catch (InterruptedException exc) {
949     }
950     render = null;
951   }
952
953   void resize (int w, int h) {
954     assert w != 0;
955     assert h != 0;
956     if (render != null) {
957       synchronized (render) {
958         width = w;
959         height = h;
960         render.notify();
961       }
962     } else {
963       width = w;
964       height = h;
965     }
966   }
967
968
969   /* We distinguish between taps and drags.
970
971      - Drags/pans (down, motion, up) are sent to the saver to handle.
972      - Single-taps exit the saver.
973      - Long-press single-taps are sent to the saver as ButtonPress/Release;
974      - Double-taps are sent to the saver as a "Space" keypress.
975
976      #### TODO:
977      - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow.
978    */
979
980   @Override
981   public boolean onSingleTapConfirmed (MotionEvent event) {
982     if (on_quit != null)
983       on_quit.run();
984     return true;
985   }
986
987   @Override
988   public boolean onDoubleTap (MotionEvent event) {
989     sendKeyEvent (new KeyEvent (KeyEvent.ACTION_DOWN,
990                                 KeyEvent.KEYCODE_SPACE));
991     return true;
992   }
993
994   @Override
995   public void onLongPress (MotionEvent event) {
996     if (! button_down_p) {
997       int x = (int) event.getX (event.getPointerId (0));
998       int y = (int) event.getY (event.getPointerId (0));
999       sendButtonEvent (x, y, true);
1000       sendButtonEvent (x, y, false);
1001     }
1002   }
1003
1004   @Override
1005   public void onShowPress (MotionEvent event) {
1006     if (! button_down_p) {
1007       button_down_p = true;
1008       int x = (int) event.getX (event.getPointerId (0));
1009       int y = (int) event.getY (event.getPointerId (0));
1010       sendButtonEvent (x, y, true);
1011     }
1012   }
1013
1014   @Override
1015   public boolean onScroll (MotionEvent e1, MotionEvent e2, 
1016                            float distanceX, float distanceY) {
1017     // LOG ("onScroll: %d", button_down_p ? 1 : 0);
1018     if (button_down_p)
1019       sendMotionEvent ((int) e2.getX (e2.getPointerId (0)),
1020                        (int) e2.getY (e2.getPointerId (0)));
1021     return true;
1022   }
1023
1024   // If you drag too fast, you get a single onFling event instead of a
1025   // succession of onScroll events.  I can't figure out how to disable it.
1026   @Override
1027   public boolean onFling (MotionEvent e1, MotionEvent e2, 
1028                           float velocityX, float velocityY) {
1029     return false;
1030   }
1031
1032   public boolean dragEnded (MotionEvent event) {
1033     if (button_down_p) {
1034       int x = (int) event.getX (event.getPointerId (0));
1035       int y = (int) event.getY (event.getPointerId (0));
1036       sendButtonEvent (x, y, false);
1037       button_down_p = false;
1038     }
1039     return true;
1040   }
1041
1042   @Override
1043   public boolean onDown (MotionEvent event) {
1044     return false;
1045   }
1046
1047   @Override
1048   public boolean onSingleTapUp (MotionEvent event) {
1049     return false;
1050   }
1051
1052   @Override
1053   public boolean onDoubleTapEvent (MotionEvent event) {
1054     return false;
1055   }
1056
1057
1058   static {
1059     System.loadLibrary ("xscreensaver");
1060
1061 /*
1062     Thread.setDefaultUncaughtExceptionHandler(
1063       new Thread.UncaughtExceptionHandler() {
1064         Thread.UncaughtExceptionHandler old_handler =
1065           Thread.currentThread().getUncaughtExceptionHandler();
1066
1067         @Override
1068         public void uncaughtException (Thread thread, Throwable ex) {
1069           String err = ex.toString();
1070           Log.d ("xscreensaver", "Caught exception: " + err);
1071           old_handler.uncaughtException (thread, ex);
1072         }
1073       });
1074 */
1075   }
1076 }