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