1 /* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2  * xscreensaver, Copyright (c) 2016-2019 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
onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)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 {
SurfaceLost()91     SurfaceLost () {
92       super("surface lost");
93     }
94 
SurfaceLost(String detailMessage)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   //
nativeInit(String hack, Hashtable<String,String> defaults, int w, int h, Surface window)137   private native long nativeInit (String hack,
138                                   Hashtable<String,String> defaults,
139                                   int w, int h, Surface window)
140                                   throws SurfaceLost;
nativeResize(int w, int h, double rot)141   private native void nativeResize (int w, int h, double rot);
nativeRender()142   private native long nativeRender ();
nativeDone()143   private native void nativeDone ();
sendButtonEvent(int x, int y, boolean down)144   public native void sendButtonEvent (int x, int y, boolean down);
sendMotionEvent(int x, int y)145   public native void sendMotionEvent (int x, int y);
sendKeyEvent(boolean down_p, int code, int mods)146   public native void sendKeyEvent (boolean down_p, int code, int mods);
147 
LOG(String fmt, Object... args)148   private void LOG (String fmt, Object... args) {
149     Log.d ("xscreensaver", hack + ": " + String.format (fmt, args));
150   }
151 
saverNameOf(Object obj)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
jwxyz(String hack, Context app, Bitmap screenshot, int w, int h, Surface surface, Runnable on_quit)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 
finalize()188   protected void finalize() {
189     if (render != null) {
190       LOG ("jwxyz finalized without close. This might be OK.");
191       close();
192     }
193   }
194 
195 
getStringResource(String name)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 
mungeFontName(String name)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 
scanSystemFonts()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.toLowerCase())) {
264             // LOG ("system font \"%s\" %s", name, file);
265             all_fonts.put (name.toLowerCase(), 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.toLowerCase(), 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 ]
parseXLFD(int mask, int traits, String name, int name_type)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 ]
parseNativeFont(String name)362   private Object[] parseNativeFont (String name) {
363     Object font2 = all_fonts.get (name.toLowerCase());
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 ]
loadFont(int mask, int traits, String name, int name_type, float size)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     if (font == null) return null;
387 
388     size *= 2;
389 
390     String suffix = (font.isBold() && font.isItalic() ? " bold italic" :
391                      font.isBold()   ? " bold"   :
392                      font.isItalic() ? " italic" :
393                      "");
394     Paint paint = new Paint();
395     paint.setTypeface (font);
396     paint.setTextSize (size);
397     paint.setColor (Color.argb (0xFF, 0xFF, 0xFF, 0xFF));
398 
399     LOG ("load font \"%s\" = \"%s %.1f\"", name, name2 + suffix, size);
400 
401     FontMetrics fm = paint.getFontMetrics();
402     return new Object[] { paint, name2, -fm.ascent, fm.descent };
403   }
404 
405 
406   /* Returns a byte[] array containing XCharStruct with an optional
407      bitmap appended to it.
408      lbearing, rbearing, width, ascent, descent: 2 bytes each.
409      Followed by a WxH pixmap, 32 bits per pixel.
410    */
renderText(Paint paint, String text, boolean render_p, boolean antialias_p)411   public ByteBuffer renderText (Paint paint, String text, boolean render_p,
412                                 boolean antialias_p) {
413 
414     if (paint == null) {
415       LOG ("no font");
416       return null;
417     }
418 
419     /* Font metric terminology, as used by X11:
420 
421        "lbearing" is the distance from the logical origin to the leftmost
422        pixel.  If a character's ink extends to the left of the origin, it is
423        negative.
424 
425        "rbearing" is the distance from the logical origin to the rightmost
426        pixel.
427 
428        "descent" is the distance from the logical origin to the bottommost
429        pixel.  For characters with descenders, it is positive.  For
430        superscripts, it is negative.
431 
432        "ascent" is the distance from the logical origin to the topmost pixel.
433        It is the number of pixels above the baseline.
434 
435        "width" is the distance from the logical origin to the position where
436        the logical origin of the next character should be placed.
437 
438        If "rbearing" is greater than "width", then this character overlaps the
439        following character.  If smaller, then there is trailing blank space.
440 
441        The bbox coordinates returned by getTextBounds grow down and right:
442        for a character with ink both above and below the baseline, top is
443        negative and bottom is positive.
444      */
445     paint.setAntiAlias (antialias_p);
446     FontMetrics fm = paint.getFontMetrics();
447     Rect bbox = new Rect();
448     paint.getTextBounds (text, 0, text.length(), bbox);
449 
450     /* The bbox returned by getTextBounds measures from the logical origin
451        with right and down being positive.  This means most characters have
452        a negative top, and characters with descenders have a positive bottom.
453      */
454     int lbearing  =  bbox.left;
455     int rbearing  =  bbox.right;
456     int ascent    = -bbox.top;
457     int descent   =  bbox.bottom;
458     int width     = (int) paint.measureText (text);
459 
460     int w = rbearing - lbearing;
461     int h = ascent + descent;
462     int size = 5 * 2 + (render_p ? w * h * 4 : 0);
463 
464     ByteBuffer bits = ByteBuffer.allocateDirect (size);
465 
466     bits.put ((byte) ((lbearing >> 8) & 0xFF));
467     bits.put ((byte) ( lbearing       & 0xFF));
468     bits.put ((byte) ((rbearing >> 8) & 0xFF));
469     bits.put ((byte) ( rbearing       & 0xFF));
470     bits.put ((byte) ((width    >> 8) & 0xFF));
471     bits.put ((byte) ( width          & 0xFF));
472     bits.put ((byte) ((ascent   >> 8) & 0xFF));
473     bits.put ((byte) ( ascent         & 0xFF));
474     bits.put ((byte) ((descent  >> 8) & 0xFF));
475     bits.put ((byte) ( descent        & 0xFF));
476 
477     if (render_p && w > 0 && h > 0) {
478       Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
479       Canvas canvas = new Canvas (bitmap);
480       canvas.drawText (text, -lbearing, ascent, paint);
481       bitmap.copyPixelsToBuffer (bits);
482       bitmap.recycle();
483     }
484 
485     return bits;
486   }
487 
488 
489   /* Returns the contents of the URL.
490      Loads the URL in a background thread: if the URL has not yet loaded,
491      this will return null.  Once the URL has completely loaded, the full
492      contents will be returned.  Calling this again after that starts the
493      URL loading again.
494    */
495   private String loading_url = null;
496   private ByteBuffer loaded_url_body = null;
497 
loadURL(String url)498   public synchronized ByteBuffer loadURL (String url) {
499 
500     if (loaded_url_body != null) {			// Thread finished
501 
502       // LOG ("textclient finished %s", loading_url);
503 
504       ByteBuffer bb = loaded_url_body;
505       loading_url = null;
506       loaded_url_body = null;
507       return bb;
508 
509     } else if (loading_url != null) {			// Waiting on thread
510       // LOG ("textclient waiting...");
511       return null;
512 
513     } else {						// Launch thread
514 
515       loading_url = url;
516       LOG ("textclient launching %s...", url);
517 
518       new Thread (new Runnable() {
519           public void run() {
520             int size0 = 10240;
521             int size = size0;
522             int count = 0;
523             ByteBuffer body = ByteBuffer.allocateDirect (size);
524 
525             try {
526               URL u = new URL (loading_url);
527               // LOG ("textclient thread loading: %s", u.toString());
528               InputStream s = u.openStream();
529               byte buf[] = new byte[10240];
530               while (true) {
531                 int n = s.read (buf);
532                 if (n == -1) break;
533                 // LOG ("textclient thread read %d", n);
534                 if (count + n + 1 >= size) {
535                   int size2 = (int) (size * 1.2 + size0);
536                   // LOG ("textclient thread expand %d -> %d", size, size2);
537                   ByteBuffer body2 = ByteBuffer.allocateDirect (size2);
538                   body.rewind();
539                   body2.put (body);
540                   body2.position (count);
541                   body = body2;
542                   size = size2;
543                 }
544                 body.put (buf, 0, n);
545                 count += n;
546               }
547             } catch (Exception e) {
548               LOG ("load URL error: %s", e.toString());
549               body.clear();
550               body.put (e.toString().getBytes());
551               body.put ((byte) 0);
552             }
553 
554             // LOG ("textclient thread finished %s (%d)", loading_url, size);
555             loaded_url_body = body;
556           }
557         }).start();
558 
559       return null;
560     }
561   }
562 
563 
564   // Returns [ Bitmap bitmap, String name ]
convertBitmap(String name, Bitmap bitmap, int target_width, int target_height, ExifInterface exif, boolean rotate_p)565   private Object[] convertBitmap (String name, Bitmap bitmap,
566                                   int target_width, int target_height,
567                                   ExifInterface exif, boolean rotate_p) {
568     if (bitmap == null) return null;
569 
570     {
571 
572       int width  = bitmap.getWidth();
573       int height = bitmap.getHeight();
574       Matrix matrix = new Matrix();
575 
576       LOG ("read image %s: %d x %d", name, width, height);
577 
578       // First rotate the image as per EXIF.
579 
580       if (exif != null) {
581         int deg = 0;
582         switch (exif.getAttributeInt (ExifInterface.TAG_ORIENTATION,
583                                       ExifInterface.ORIENTATION_NORMAL)) {
584         case ExifInterface.ORIENTATION_ROTATE_90:  deg = 90;  break;
585         case ExifInterface.ORIENTATION_ROTATE_180: deg = 180; break;
586         case ExifInterface.ORIENTATION_ROTATE_270: deg = 270; break;
587         }
588         if (deg != 0) {
589           LOG ("%s: EXIF rotate %d", name, deg);
590           matrix.preRotate (deg);
591           if (deg == 90 || deg == 270) {
592             int temp = width;
593             width = height;
594             height = temp;
595           }
596         }
597       }
598 
599       // If the caller requested that we rotate the image to best fit the
600       // screen, rotate it again.
601 
602       if (rotate_p &&
603           (width > height) != (target_width > target_height)) {
604         LOG ("%s: rotated to fit screen", name);
605         matrix.preRotate (90);
606 
607         int temp = width;
608         width = height;
609         height = temp;
610       }
611 
612       // Resize the image to be not larger than the screen, potentially
613       // copying it for the third time.
614       // Actually, always scale it, scaling up if necessary.
615 
616 //    if (width > target_width || height > target_height)
617       {
618         float r1 = target_width  / (float) width;
619         float r2 = target_height / (float) height;
620         float r = (r1 > r2 ? r2 : r1);
621         LOG ("%s: resize %.1f: %d x %d => %d x %d", name,
622              r, width, height, (int) (width * r), (int) (height * r));
623         matrix.preScale (r, r);
624       }
625 
626       bitmap =  Bitmap.createBitmap (bitmap, 0, 0,
627                                      bitmap.getWidth(), bitmap.getHeight(),
628                                      matrix, true);
629 
630       if (bitmap.getConfig() != Bitmap.Config.ARGB_8888)
631         bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false);
632 
633       return new Object[] { bitmap, name };
634 
635     }
636   }
637 
638 
havePermission(String permission)639   boolean havePermission(String permission) {
640 
641         if (Build.VERSION.SDK_INT < 16) {
642             return true;
643         }
644 
645         if (permissionGranted(permission)) {
646             return true;
647         }
648 
649         return false;
650   }
651 
652 
permissionGranted(String permission)653   private boolean permissionGranted(String permission) {
654         boolean check = ContextCompat.checkSelfPermission(app, permission) ==
655                 PackageManager.PERMISSION_GRANTED;
656         return check;
657   }
658 
checkThenLoadRandomImage(int target_width, int target_height, boolean rotate_p)659   public Object[] checkThenLoadRandomImage (int target_width, int target_height,
660                                    boolean rotate_p) {
661       // RES introduced in API 16
662       String permission = Manifest.permission.READ_EXTERNAL_STORAGE;
663 
664         if (havePermission(permission)) {
665             return loadRandomImage(target_width,target_height,rotate_p);
666         } else {
667             return null;
668         }
669   }
670 
loadRandomImage(int target_width, int target_height, boolean rotate_p)671   public Object[] loadRandomImage (int target_width, int target_height,
672                                    boolean rotate_p) {
673 
674     int min_size = 480;
675     int max_size = 0x7FFF;
676 
677     ArrayList<String> imgs = new ArrayList<String>();
678 
679     ContentResolver cr = app.getContentResolver();
680     String[] cols = { MediaColumns.DATA,
681                       MediaColumns.MIME_TYPE,
682                       MediaColumns.WIDTH,
683                       MediaColumns.HEIGHT };
684     Uri uris[] = {
685       android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI,
686       android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI };
687 
688     for (int i = 0; i < uris.length; i++) {
689       Cursor cursor = cr.query (uris[i], cols, null, null, null);
690       if (cursor == null)
691         continue;
692       int j = 0;
693       int path_col   = cursor.getColumnIndexOrThrow (cols[j++]);
694       int type_col   = cursor.getColumnIndexOrThrow (cols[j++]);
695       int width_col  = cursor.getColumnIndexOrThrow (cols[j++]);
696       int height_col = cursor.getColumnIndexOrThrow (cols[j++]);
697       while (cursor.moveToNext()) {
698         String path = cursor.getString(path_col);
699         String type = cursor.getString(type_col);
700         if (path != null && type != null && type.startsWith("image/")) {
701           String wc = cursor.getString(width_col);
702           String hc = cursor.getString(height_col);
703           if (wc != null && hc != null) {
704             int w = Integer.parseInt (wc);
705             int h = Integer.parseInt (hc);
706             if (w > min_size && h > min_size &&
707                 w < max_size && h < max_size) {
708               imgs.add (path);
709             }
710           }
711         }
712       }
713       cursor.close();
714     }
715 
716     String which = null;
717 
718     int count = imgs.size();
719     if (count == 0) {
720       LOG ("no images");
721       return null;
722     }
723 
724     int i = new Random().nextInt (count);
725     which = imgs.get (i);
726     LOG ("picked image %d of %d: %s", i, count, which);
727 
728     Uri uri = Uri.fromFile (new File (which));
729     String name = uri.getLastPathSegment();
730     Bitmap bitmap = null;
731     ExifInterface exif = null;
732 
733     try {
734       try {
735         bitmap = MediaStore.Images.Media.getBitmap (cr, uri);
736       } catch (Exception e) {
737         LOG ("image %s unloadable: %s", which, e.toString());
738         return null;
739       }
740 
741       try {
742         exif = new ExifInterface (uri.getPath());  // If it fails, who cares
743       } catch (Exception e) {
744       }
745 
746       return convertBitmap (name, bitmap, target_width, target_height,
747                             exif, rotate_p);
748     } catch (java.lang.OutOfMemoryError e) {
749       LOG ("image %s got OutOfMemoryError: %s", which, e.toString());
750       return null;
751     }
752   }
753 
754 
getScreenshot(int target_width, int target_height, boolean rotate_p)755   public Object[] getScreenshot (int target_width, int target_height,
756                                boolean rotate_p) {
757     return convertBitmap ("Screenshot", screenshot,
758                           target_width, target_height,
759                           null, rotate_p);
760   }
761 
762 
decodePNG(byte[] data)763   public Bitmap decodePNG (byte[] data) {
764     BitmapFactory.Options opts = new BitmapFactory.Options();
765     opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
766     return BitmapFactory.decodeByteArray (data, 0, data.length, opts);
767   }
768 
769 
770   // Sadly duplicated from jwxyz.h (and thence X.h and keysymdef.h)
771   //
772   private static final int ShiftMask =	   (1<<0);
773   private static final int LockMask =	   (1<<1);
774   private static final int ControlMask =   (1<<2);
775   private static final int Mod1Mask =	   (1<<3);
776   private static final int Mod2Mask =	   (1<<4);
777   private static final int Mod3Mask =	   (1<<5);
778   private static final int Mod4Mask =	   (1<<6);
779   private static final int Mod5Mask =	   (1<<7);
780   private static final int Button1Mask =   (1<<8);
781   private static final int Button2Mask =   (1<<9);
782   private static final int Button3Mask =   (1<<10);
783   private static final int Button4Mask =   (1<<11);
784   private static final int Button5Mask =   (1<<12);
785 
786   private static final int XK_Shift_L =	   0xFFE1;
787   private static final int XK_Shift_R =	   0xFFE2;
788   private static final int XK_Control_L =  0xFFE3;
789   private static final int XK_Control_R =  0xFFE4;
790   private static final int XK_Caps_Lock =  0xFFE5;
791   private static final int XK_Shift_Lock = 0xFFE6;
792   private static final int XK_Meta_L =	   0xFFE7;
793   private static final int XK_Meta_R =	   0xFFE8;
794   private static final int XK_Alt_L =	   0xFFE9;
795   private static final int XK_Alt_R =	   0xFFEA;
796   private static final int XK_Super_L =	   0xFFEB;
797   private static final int XK_Super_R =	   0xFFEC;
798   private static final int XK_Hyper_L =	   0xFFED;
799   private static final int XK_Hyper_R =	   0xFFEE;
800 
801   private static final int XK_Home =	   0xFF50;
802   private static final int XK_Left =	   0xFF51;
803   private static final int XK_Up =	   0xFF52;
804   private static final int XK_Right =	   0xFF53;
805   private static final int XK_Down =	   0xFF54;
806   private static final int XK_Prior =	   0xFF55;
807   private static final int XK_Page_Up =	   0xFF55;
808   private static final int XK_Next =	   0xFF56;
809   private static final int XK_Page_Down =  0xFF56;
810   private static final int XK_End =	   0xFF57;
811   private static final int XK_Begin =	   0xFF58;
812 
813   private static final int XK_F1 =	   0xFFBE;
814   private static final int XK_F2 =	   0xFFBF;
815   private static final int XK_F3 =	   0xFFC0;
816   private static final int XK_F4 =	   0xFFC1;
817   private static final int XK_F5 =	   0xFFC2;
818   private static final int XK_F6 =	   0xFFC3;
819   private static final int XK_F7 =	   0xFFC4;
820   private static final int XK_F8 =	   0xFFC5;
821   private static final int XK_F9 =	   0xFFC6;
822   private static final int XK_F10 =	   0xFFC7;
823   private static final int XK_F11 =	   0xFFC8;
824   private static final int XK_F12 =	   0xFFC9;
825 
sendKeyEvent(KeyEvent event)826   public void sendKeyEvent (KeyEvent event) {
827     int uc    = event.getUnicodeChar();
828     int jcode = event.getKeyCode();
829     int jmods = event.getModifiers();
830     int xcode = 0;
831     int xmods = 0;
832 
833     switch (jcode) {
834     case KeyEvent.KEYCODE_SHIFT_LEFT:	     xcode = XK_Shift_L;   break;
835     case KeyEvent.KEYCODE_SHIFT_RIGHT:	     xcode = XK_Shift_R;   break;
836     case KeyEvent.KEYCODE_CTRL_LEFT:	     xcode = XK_Control_L; break;
837     case KeyEvent.KEYCODE_CTRL_RIGHT:	     xcode = XK_Control_R; break;
838     case KeyEvent.KEYCODE_CAPS_LOCK:	     xcode = XK_Caps_Lock; break;
839     case KeyEvent.KEYCODE_META_LEFT:	     xcode = XK_Meta_L;	   break;
840     case KeyEvent.KEYCODE_META_RIGHT:	     xcode = XK_Meta_R;	   break;
841     case KeyEvent.KEYCODE_ALT_LEFT:	     xcode = XK_Alt_L;	   break;
842     case KeyEvent.KEYCODE_ALT_RIGHT:	     xcode = XK_Alt_R;	   break;
843 
844     case KeyEvent.KEYCODE_HOME:		     xcode = XK_Home;	   break;
845     case KeyEvent.KEYCODE_DPAD_LEFT:	     xcode = XK_Left;	   break;
846     case KeyEvent.KEYCODE_DPAD_UP:	     xcode = XK_Up;	   break;
847     case KeyEvent.KEYCODE_DPAD_RIGHT:	     xcode = XK_Right;	   break;
848     case KeyEvent.KEYCODE_DPAD_DOWN:	     xcode = XK_Down;	   break;
849   //case KeyEvent.KEYCODE_NAVIGATE_PREVIOUS: xcode = XK_Prior;	   break;
850     case KeyEvent.KEYCODE_PAGE_UP:	     xcode = XK_Page_Up;   break;
851   //case KeyEvent.KEYCODE_NAVIGATE_NEXT:     xcode = XK_Next;	   break;
852     case KeyEvent.KEYCODE_PAGE_DOWN:	     xcode = XK_Page_Down; break;
853     case KeyEvent.KEYCODE_MOVE_END:	     xcode = XK_End;	   break;
854     case KeyEvent.KEYCODE_MOVE_HOME:	     xcode = XK_Begin;	   break;
855 
856     case KeyEvent.KEYCODE_F1:		     xcode = XK_F1;	   break;
857     case KeyEvent.KEYCODE_F2:		     xcode = XK_F2;	   break;
858     case KeyEvent.KEYCODE_F3:		     xcode = XK_F3;	   break;
859     case KeyEvent.KEYCODE_F4:		     xcode = XK_F4;	   break;
860     case KeyEvent.KEYCODE_F5:		     xcode = XK_F5;	   break;
861     case KeyEvent.KEYCODE_F6:		     xcode = XK_F6;	   break;
862     case KeyEvent.KEYCODE_F7:		     xcode = XK_F7;	   break;
863     case KeyEvent.KEYCODE_F8:		     xcode = XK_F8;	   break;
864     case KeyEvent.KEYCODE_F9:		     xcode = XK_F9;	   break;
865     case KeyEvent.KEYCODE_F10:		     xcode = XK_F10;	   break;
866     case KeyEvent.KEYCODE_F11:		     xcode = XK_F11;	   break;
867     case KeyEvent.KEYCODE_F12:		     xcode = XK_F12;	   break;
868     default:				     xcode = uc;	   break;
869     }
870 
871     if (0 != (jmods & KeyEvent.META_SHIFT_ON))     xmods |= ShiftMask;
872     if (0 != (jmods & KeyEvent.META_CAPS_LOCK_ON)) xmods |= LockMask;
873     if (0 != (jmods & KeyEvent.META_CTRL_MASK))    xmods |= ControlMask;
874     if (0 != (jmods & KeyEvent.META_ALT_MASK))	   xmods |= Mod1Mask;
875     if (0 != (jmods & KeyEvent.META_META_ON))      xmods |= Mod1Mask;
876     if (0 != (jmods & KeyEvent.META_SYM_ON))       xmods |= Mod2Mask;
877     if (0 != (jmods & KeyEvent.META_FUNCTION_ON))  xmods |= Mod3Mask;
878 
879     /* If you touch and release Shift, you get no events.
880        If you type Shift-A, you get Shift down, A down, A up, Shift up.
881        So let's just ignore all lone modifier key events.
882      */
883     if (xcode >= XK_Shift_L && xcode <= XK_Hyper_R)
884       return;
885 
886     boolean down_p = event.getAction() == KeyEvent.ACTION_DOWN;
887     sendKeyEvent (down_p, xcode, xmods);
888   }
889 
start()890   void start () {
891     if (render == null) {
892       animating_p = true;
893       render = new Thread(new Runnable() {
894         @Override
895         public void run()
896         {
897           int currentWidth, currentHeight;
898           synchronized (render) {
899             while (true) {
900               while (!animating_p || width == 0 || height == 0) {
901                 try {
902                   render.wait();
903                 } catch(InterruptedException exc) {
904                   return;
905                 }
906               }
907 
908               try {
909                 nativeInit (hack, defaults, width, height, surface);
910                 currentWidth = width;
911                 currentHeight= height;
912                 break;
913               } catch (SurfaceLost exc) {
914                 width = 0;
915                 height = 0;
916               }
917             }
918           }
919 
920         main_loop:
921           while (true) {
922             synchronized (render) {
923               assert width != 0;
924               assert height != 0;
925               while (!animating_p) {
926                 try {
927                   render.wait();
928                 } catch(InterruptedException exc) {
929                   break main_loop;
930                 }
931               }
932 
933               if (currentWidth != width || currentHeight != height) {
934                 currentWidth = width;
935                 currentHeight = height;
936                 nativeResize (width, height, 0);
937               }
938             }
939 
940             long delay = nativeRender();
941 
942             synchronized (render) {
943               if (delay != 0) {
944                 try {
945                   render.wait(delay / 1000, (int)(delay % 1000) * 1000);
946                 } catch (InterruptedException exc) {
947                   break main_loop;
948                 }
949               } else {
950                 if (Thread.interrupted ()) {
951                   break main_loop;
952                 }
953               }
954             }
955           }
956 
957           assert nativeRunningHackPtr != 0;
958           nativeDone ();
959         }
960       });
961 
962       render.start();
963     } else {
964       synchronized(render) {
965         animating_p = true;
966         render.notify();
967       }
968     }
969   }
970 
pause()971   void pause () {
972     if (render == null)
973       return;
974     synchronized (render) {
975       animating_p = false;
976       render.notify();
977     }
978   }
979 
close()980   void close () {
981     if (render == null)
982       return;
983     synchronized (render) {
984       animating_p = false;
985       render.interrupt();
986     }
987     try {
988       render.join();
989     } catch (InterruptedException exc) {
990     }
991     render = null;
992   }
993 
resize(int w, int h)994   void resize (int w, int h) {
995     assert w != 0;
996     assert h != 0;
997     if (render != null) {
998       synchronized (render) {
999         width = w;
1000         height = h;
1001         render.notify();
1002       }
1003     } else {
1004       width = w;
1005       height = h;
1006     }
1007   }
1008 
1009 
1010   /* We distinguish between taps and drags.
1011 
1012      - Drags/pans (down, motion, up) are sent to the saver to handle.
1013      - Single-taps exit the saver.
1014      - Long-press single-taps are sent to the saver as ButtonPress/Release;
1015      - Double-taps are sent to the saver as a "Space" keypress.
1016 
1017      #### TODO:
1018      - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow.
1019    */
1020 
1021   @Override
onSingleTapConfirmed(MotionEvent event)1022   public boolean onSingleTapConfirmed (MotionEvent event) {
1023     if (on_quit != null)
1024       on_quit.run();
1025     return true;
1026   }
1027 
1028   @Override
onDoubleTap(MotionEvent event)1029   public boolean onDoubleTap (MotionEvent event) {
1030     sendKeyEvent (new KeyEvent (KeyEvent.ACTION_DOWN,
1031                                 KeyEvent.KEYCODE_SPACE));
1032     return true;
1033   }
1034 
1035   @Override
onLongPress(MotionEvent event)1036   public void onLongPress (MotionEvent event) {
1037     if (! button_down_p) {
1038       int x = (int) event.getX (event.getPointerId (0));
1039       int y = (int) event.getY (event.getPointerId (0));
1040       sendButtonEvent (x, y, true);
1041       sendButtonEvent (x, y, false);
1042     }
1043   }
1044 
1045   @Override
onShowPress(MotionEvent event)1046   public void onShowPress (MotionEvent event) {
1047     if (! button_down_p) {
1048       button_down_p = true;
1049       int x = (int) event.getX (event.getPointerId (0));
1050       int y = (int) event.getY (event.getPointerId (0));
1051       sendButtonEvent (x, y, true);
1052     }
1053   }
1054 
1055   @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)1056   public boolean onScroll (MotionEvent e1, MotionEvent e2,
1057                            float distanceX, float distanceY) {
1058     // LOG ("onScroll: %d", button_down_p ? 1 : 0);
1059     if (button_down_p)
1060       sendMotionEvent ((int) e2.getX (e2.getPointerId (0)),
1061                        (int) e2.getY (e2.getPointerId (0)));
1062     return true;
1063   }
1064 
1065   // If you drag too fast, you get a single onFling event instead of a
1066   // succession of onScroll events.  I can't figure out how to disable it.
1067   @Override
onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)1068   public boolean onFling (MotionEvent e1, MotionEvent e2,
1069                           float velocityX, float velocityY) {
1070     return false;
1071   }
1072 
dragEnded(MotionEvent event)1073   public boolean dragEnded (MotionEvent event) {
1074     if (button_down_p) {
1075       int x = (int) event.getX (event.getPointerId (0));
1076       int y = (int) event.getY (event.getPointerId (0));
1077       sendButtonEvent (x, y, false);
1078       button_down_p = false;
1079     }
1080     return true;
1081   }
1082 
1083   @Override
onDown(MotionEvent event)1084   public boolean onDown (MotionEvent event) {
1085     return false;
1086   }
1087 
1088   @Override
onSingleTapUp(MotionEvent event)1089   public boolean onSingleTapUp (MotionEvent event) {
1090     return false;
1091   }
1092 
1093   @Override
onDoubleTapEvent(MotionEvent event)1094   public boolean onDoubleTapEvent (MotionEvent event) {
1095     return false;
1096   }
1097 
1098 
1099   static {
1100     System.loadLibrary ("xscreensaver");
1101 
1102 /*
1103     Thread.setDefaultUncaughtExceptionHandler(
1104       new Thread.UncaughtExceptionHandler() {
1105         Thread.UncaughtExceptionHandler old_handler =
1106           Thread.currentThread().getUncaughtExceptionHandler();
1107 
1108         @Override
1109         public void uncaughtException (Thread thread, Throwable ex) {
1110           String err = ex.toString();
1111           Log.d ("xscreensaver", "Caught exception: " + err);
1112           old_handler.uncaughtException (thread, ex);
1113         }
1114       });
1115 */
1116   }
1117 }
1118