From http://www.jwz.org/xscreensaver/xscreensaver-5.36.tar.gz
[xscreensaver] / android / project / xscreensaver / src / org / jwz / xscreensaver / jwxyz.java
1 /* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2  * xscreensaver, Copyright (c) 2016 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.view.KeyEvent;
25 import android.content.SharedPreferences;
26 import android.content.Context;
27 import android.content.ContentResolver;
28 import android.content.res.AssetManager;
29 import android.graphics.Typeface;
30 import android.graphics.Rect;
31 import android.graphics.Paint;
32 import android.graphics.Paint.FontMetrics;
33 import android.graphics.Bitmap;
34 import android.graphics.Canvas;
35 import android.graphics.Color;
36 import android.graphics.Matrix;
37 import android.net.Uri;
38 import java.net.URL;
39 import java.nio.ByteBuffer;
40 import java.io.File;
41 import java.io.InputStream;
42 import java.io.FileOutputStream;
43 import java.lang.Runnable;
44 import java.lang.Thread;
45 import android.database.Cursor;
46 import android.provider.MediaStore;
47 import android.provider.MediaStore.MediaColumns;
48 import android.media.ExifInterface;
49 import org.jwz.xscreensaver.TTFAnalyzer;
50 import android.util.Log;
51
52 public class jwxyz {
53
54   private void LOG (String fmt, Object... args) {
55     Log.d ("xscreensaver",
56            this.getClass().getSimpleName() + ": " +
57            String.format (fmt, args));
58   }
59
60   private long nativeRunningHackPtr;
61
62   String hack;
63   Context app;
64   public final static int API_XLIB = 0;
65   public final static int API_GL   = 1;
66   Bitmap screenshot;
67
68   SharedPreferences prefs;
69   Hashtable<String, String> defaults = new Hashtable<String, String>();
70
71
72   // Maps font names to either: String (system font) or Typeface (bundled).
73   private Hashtable<String, Object> all_fonts =
74     new Hashtable<String, Object>();
75
76
77   // These are defined in jwxyz-android.c:
78   //
79   private native void nativeInit (String hack, int api,
80                                   Hashtable<String,String> defaults,
81                                   int w, int h);
82   public native void nativeResize (int w, int h, double rot);
83   public native long nativeRender ();
84   public native void nativeDone ();
85   public native void sendButtonEvent (int x, int y, boolean down);
86   public native void sendMotionEvent (int x, int y);
87   public native void sendKeyEvent (boolean down_p, int code, int mods);
88
89   // Constructor
90   public jwxyz (String hack, int api, Context app, Bitmap screenshot,
91                 int w, int h) {
92
93     this.hack = hack;
94     this.app  = app;
95     this.screenshot = screenshot;
96
97     // nativeInit populates 'defaults' with the default values for keys
98     // that are not overridden by SharedPreferences.
99
100     prefs = app.getSharedPreferences (hack, 0);
101     scanSystemFonts();
102     nativeInit (hack, api, defaults, w, h);
103   }
104
105 /*  TODO: Can't do this yet; nativeDone requires the OpenGL context to be set.
106   protected void finalize() {
107     if (nativeRunningHackPtr != 0) {
108       nativeDone();
109     }
110   } */
111
112
113   public String getStringResource (String name) {
114
115     name = hack + "_" + name;
116
117     if (prefs.contains(name)) {
118
119       // SharedPreferences is very picky that you request the exact type that
120       // was stored: if it is a float and you ask for a string, you get an
121       // exception instead of the float converted to a string.
122
123       String s = null;
124       try { return prefs.getString (name, "");
125       } catch (Exception e) { }
126
127       try { return Float.toString (prefs.getFloat (name, 0));
128       } catch (Exception e) { }
129
130       try { return Long.toString (prefs.getLong (name, 0));
131       } catch (Exception e) { }
132
133       try { return Integer.toString (prefs.getInt (name, 0));
134       } catch (Exception e) { }
135
136       try { return (prefs.getBoolean (name, false) ? "true" : "false");
137       } catch (Exception e) { }
138     }
139
140     // If we got to here, it's not in there, so return the default.
141     return defaults.get (name);
142   }
143
144
145   private String mungeFontName (String name) {
146     // Roboto-ThinItalic => RobotoThin
147     // AndroidCock Regular => AndroidClock
148     String tails[] = { "Bold", "Italic", "Oblique", "Regular" };
149     for (String tail : tails) {
150       String pres[] = { " ", "-", "_", "" };
151       for (String pre : pres) {
152         int i = name.indexOf(pre + tail);
153         if (i > 0) name = name.substring (0, i);
154       }
155     }
156     return name;
157   }
158
159
160   private void scanSystemFonts() {
161
162     // First parse the system font directories for the global fonts.
163
164     String[] fontdirs = { "/system/fonts", "/system/font", "/data/fonts" };
165     TTFAnalyzer analyzer = new TTFAnalyzer();
166     for (String fontdir : fontdirs) {
167       File dir = new File(fontdir);
168       if (!dir.exists())
169         continue;
170       File[] files = dir.listFiles();
171       if (files == null)
172         continue;
173
174       for (File file : files) {
175         String name = analyzer.getTtfFontName (file.getAbsolutePath());
176         if (name == null) {
177           // LOG ("unparsable system font: %s", file);
178         } else {
179           name = mungeFontName (name);
180           if (! all_fonts.contains (name)) {
181             // LOG ("system font \"%s\" %s", name, file);
182             all_fonts.put (name, name);
183           }
184         }
185       }
186     }
187
188     // Now parse our assets, for our bundled fonts.
189
190     AssetManager am = app.getAssets();
191     String dir = "fonts";
192     String[] files = null;
193     try { files = am.list(dir); }
194     catch (Exception e) { LOG("listing assets: %s", e.toString()); }
195
196     for (String fn : files) {
197       String fn2 = dir + "/" + fn;
198       Typeface t = Typeface.createFromAsset (am, fn2);
199
200       File tmpfile = null;
201       try {
202         tmpfile = new File(app.getCacheDir(), fn);
203         if (tmpfile.createNewFile() == false) {
204           tmpfile.delete();
205           tmpfile.createNewFile();
206         }
207
208         InputStream in = am.open (fn2);
209         FileOutputStream out = new FileOutputStream (tmpfile);
210         byte[] buffer = new byte[1024 * 512];
211         while (in.read(buffer, 0, 1024 * 512) != -1) {
212           out.write(buffer);
213         }
214         out.close();
215         in.close();
216
217         String name = analyzer.getTtfFontName (tmpfile.getAbsolutePath());
218         tmpfile.delete();
219
220         name = mungeFontName (name);
221         all_fonts.put (name, t);
222         // LOG ("asset font \"%s\" %s", name, fn);
223       } catch (Exception e) {
224         if (tmpfile != null) tmpfile.delete();
225         LOG ("error: %s", e.toString());
226       }
227     }
228   }
229
230
231   // Parses X Logical Font Descriptions, and a few standard X font names.
232   // Returns [ String name, Float size, Typeface ]
233   private Object[] parseXLFD (String name) {
234     float   size   = 12;
235     boolean bold   = false;
236     boolean italic = false;
237     boolean fixed  = false;
238     boolean serif  = false;
239
240     if      (name.equals("6x10"))     { size = 8;  fixed = true; }
241     else if (name.equals("6x10bold")) { size = 8;  fixed = true; bold = true; }
242     else if (name.equals("fixed"))    { size = 12; fixed = true; }
243     else if (name.equals("9x15"))     { size = 12; fixed = true; }
244     else if (name.equals("9x15bold")) { size = 12; fixed = true; bold = true; }
245     else if (name.equals("vga"))      { size = 12; fixed = true; }
246     else if (name.equals("console"))  { size = 12; fixed = true; }
247     else if (name.equals("gallant"))  { size = 12; fixed = true; }
248     else {
249       String[] tokens = name.split("-");        // XLFD
250       int L = tokens.length;
251       int i = 1;
252       String foundry  = (i < L ? tokens[i++] : "");
253       String family   = (i < L ? tokens[i++] : "");
254       String weight   = (i < L ? tokens[i++] : "");
255       String slant    = (i < L ? tokens[i++] : "");
256       String setwidth = (i < L ? tokens[i++] : "");
257       String adstyle  = (i < L ? tokens[i++] : "");
258       String pxsize   = (i < L ? tokens[i++] : "");
259       String ptsize   = (i < L ? tokens[i++] : "");
260       String resx     = (i < L ? tokens[i++] : "");
261       String resy     = (i < L ? tokens[i++] : "");
262       String spacing  = (i < L ? tokens[i++] : "");
263       String avgw     = (i < L ? tokens[i++] : "");
264       String charset  = (i < L ? tokens[i++] : "");
265       String registry = (i < L ? tokens[i++] : "");
266
267       if (spacing.equals("m") ||
268           family.equals("fixed") ||
269           family.equals("courier") ||
270           family.equals("console") ||
271           family.equals("lucidatypewriter")) {
272         fixed = true;
273       } else if (family.equals("times") ||
274                  family.equals("georgia")) {
275         serif = true;
276       }
277
278       if (weight.equals("bold") || weight.equals("demibold")) {
279         bold = true; 
280       }
281
282       if (slant.equals("i") || slant.equals("o")) {
283         italic = true;
284       }
285
286       // -*-courier-bold-r-*-*-14-*-*-*-*-*-*-*         14 px
287       // -*-courier-bold-r-*-*-*-140-*-*-m-*-*-*        14 pt
288       // -*-courier-bold-r-*-*-140-*                    14 pt, via wildcard
289       // -*-courier-bold-r-*-140-*                      14 pt, not handled
290       // -*-courier-bold-r-*-*-14-180-*-*-*-*-*-*       error
291
292       if (!ptsize.equals("") && !ptsize.equals("*")) {
293         // It was in the ptsize field, so that's definitely what it is.
294         size = Float.valueOf(ptsize) / 10.0f;
295       } else if (!pxsize.equals("") && !pxsize.equals("*")) {
296         size = Float.valueOf(pxsize);
297         // If it's a fully qualified XLFD, then this really is the pxsize.
298         // Otherwise, this is probably point size with a multi-field wildcard.
299         if (registry.equals(""))   // not a fully qualified XLFD
300           size /= 10.0f;
301       }
302     }
303
304     if (name.equals("random")) {
305       Random r = new Random();
306       serif = r.nextBoolean();      // Not much to randomize here...
307       fixed = (r.nextInt(8) == 0);
308     }
309
310     name = (fixed
311             ? (serif ? "serif-monospace" : "monospace")
312             : (serif ? "serif" : "sans-serif"));
313
314     Typeface font = Typeface.create (name,
315                                      (bold && italic ? Typeface.BOLD_ITALIC :
316                                       bold   ? Typeface.BOLD :
317                                       italic ? Typeface.ITALIC :
318                                       Typeface.NORMAL));
319
320     Object ret[] = { name, new Float(size), font };
321     return ret;
322   }
323
324
325   // Parses "Native Font Name One 12, Native Font Name Two 14".
326   // Returns [ String name, Float size, Typeface ]
327   private Object[] parseNativeFont (String names) {
328     for (String name : names.split(",")) {
329       float size = 0;
330       name = name.trim();
331       if (name.equals("")) continue;
332       int spc = name.lastIndexOf(" ");
333       if (spc > 0) {
334         size = Float.valueOf (name.substring (spc + 1));
335         name = name.substring (0, spc);
336       }
337       if (size <= 0)
338         size = 12;
339
340       Object font = all_fonts.get (name);
341       if (font instanceof String)
342         font = Typeface.create (name, Typeface.NORMAL);
343
344       if (font != null) {
345         Object ret[] = { name, size, (Typeface) font };
346         return ret;
347       }
348     }
349
350     return null;
351   }
352
353
354   // Returns [ Paint paint, String name, Float size, ascent, descent ]
355   public Object[] loadFont(String name) {
356     Object pair[];
357
358     if (name.equals("")) return null;
359
360     if (name.contains(" ")) {
361       pair = parseNativeFont (name);
362     } else {
363       pair = parseXLFD (name);
364     }
365
366     if (pair == null) return null;
367
368     String name2  = (String)   pair[0];
369     float size    = (Float)    pair[1];
370     Typeface font = (Typeface) pair[2];
371
372     size *= 2;
373
374     name2 += (font.isBold() && font.isItalic() ? " bold italic" :
375               font.isBold()   ? " bold"   :
376               font.isItalic() ? " italic" :
377               "");
378     Paint paint = new Paint();
379     paint.setTypeface (font);
380     paint.setTextSize (size);
381     paint.setColor (Color.argb (0xFF, 0xFF, 0xFF, 0xFF));
382
383     LOG ("load font \"%s\" = \"%s %.1f\"", name, name2, size);
384
385     FontMetrics fm = paint.getFontMetrics();
386     Object ret[] = { paint, name2, new Float(size),
387                      new Float(-fm.ascent), new Float(fm.descent) };
388     return ret;
389   }
390
391
392   /* Returns a byte[] array containing XCharStruct with an optional
393      bitmap appended to it.
394      lbearing, rbearing, width, ascent, descent: 2 bytes each.
395      Followed by a WxH pixmap, 32 bits per pixel.
396    */
397   public ByteBuffer renderText (Paint paint, String text, boolean render_p) {
398
399     if (paint == null) {
400       LOG ("no font");
401       return null;
402     }
403
404     /* Font metric terminology, as used by X11:
405
406        "lbearing" is the distance from the logical origin to the leftmost
407        pixel.  If a character's ink extends to the left of the origin, it is
408        negative.
409
410        "rbearing" is the distance from the logical origin to the rightmost
411        pixel.
412
413        "descent" is the distance from the logical origin to the bottommost
414        pixel.  For characters with descenders, it is positive.  For
415        superscripts, it is negative.
416
417        "ascent" is the distance from the logical origin to the topmost pixel.
418        It is the number of pixels above the baseline.
419
420        "width" is the distance from the logical origin to the position where
421        the logical origin of the next character should be placed.
422
423        If "rbearing" is greater than "width", then this character overlaps the
424        following character.  If smaller, then there is trailing blank space.
425
426        The bbox coordinates returned by getTextBounds grow down and right:
427        for a character with ink both above and below the baseline, top is
428        negative and bottom is positive.
429      */
430     FontMetrics fm = paint.getFontMetrics();
431     Rect bbox = new Rect();
432     paint.getTextBounds (text, 0, text.length(), bbox);
433
434     /* The bbox returned by getTextBounds measures from the logical origin
435        with right and down being positive.  This means most characters have
436        a negative top, and characters with descenders have a positive bottom.
437      */
438     int lbearing  =  bbox.left;
439     int rbearing  =  bbox.right;
440     int ascent    = -bbox.top;
441     int descent   =  bbox.bottom;
442     int width     = (int) paint.measureText (text);
443
444     int w = rbearing - lbearing;
445     int h = ascent + descent;
446     int size = 5 * 2 + (render_p ? w * h * 4 : 0);
447
448     ByteBuffer bits = ByteBuffer.allocateDirect (size);
449
450     bits.put ((byte) ((lbearing >> 8) & 0xFF));
451     bits.put ((byte) ( lbearing       & 0xFF));
452     bits.put ((byte) ((rbearing >> 8) & 0xFF));
453     bits.put ((byte) ( rbearing       & 0xFF));
454     bits.put ((byte) ((width    >> 8) & 0xFF));
455     bits.put ((byte) ( width          & 0xFF));
456     bits.put ((byte) ((ascent   >> 8) & 0xFF));
457     bits.put ((byte) ( ascent         & 0xFF));
458     bits.put ((byte) ((descent  >> 8) & 0xFF));
459     bits.put ((byte) ( descent        & 0xFF));
460
461     if (render_p && w > 0 && h > 0) {
462       Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
463       Canvas canvas = new Canvas (bitmap);
464       canvas.drawText (text, -lbearing, ascent, paint);
465       bitmap.copyPixelsToBuffer (bits);
466       bitmap.recycle();
467     }
468
469     return bits;
470   }
471
472
473   /* Returns the contents of the URL.
474      Loads the URL in a background thread: if the URL has not yet loaded,
475      this will return null.  Once the URL has completely loaded, the full
476      contents will be returned.  Calling this again after that starts the
477      URL loading again.
478    */
479   private String loading_url = null;
480   private ByteBuffer loaded_url_body = null;
481
482   public synchronized ByteBuffer loadURL (String url) {
483
484     if (loaded_url_body != null) {                      // Thread finished
485
486       // LOG ("textclient finished %s", loading_url);
487
488       ByteBuffer bb = loaded_url_body;
489       loading_url = null;
490       loaded_url_body = null;
491       return bb;
492
493     } else if (loading_url != null) {                   // Waiting on thread
494       // LOG ("textclient waiting...");
495       return null;
496
497     } else {                                            // Launch thread
498
499       loading_url = url;
500       LOG ("textclient launching %s...", url);
501
502       new Thread (new Runnable() {
503           public void run() {
504             int size0 = 10240;
505             int size = size0;
506             int count = 0;
507             ByteBuffer body = ByteBuffer.allocateDirect (size);
508
509             try {
510               URL u = new URL (loading_url);
511               // LOG ("textclient thread loading: %s", u.toString());
512               InputStream s = u.openStream();
513               byte buf[] = new byte[10240];
514               while (true) {
515                 int n = s.read (buf);
516                 if (n == -1) break;
517                 // LOG ("textclient thread read %d", n);
518                 if (count + n + 1 >= size) {
519                   int size2 = (int) (size * 1.2 + size0);
520                   // LOG ("textclient thread expand %d -> %d", size, size2);
521                   ByteBuffer body2 = ByteBuffer.allocateDirect (size2);
522                   body.rewind();
523                   body2.put (body);
524                   body2.position (count);
525                   body = body2;
526                   size = size2;
527                 }
528                 body.put (buf, 0, n);
529                 count += n;
530               }
531             } catch (Exception e) {
532               LOG ("load URL error: %s", e.toString());
533               body.clear();
534               body.put (e.toString().getBytes());
535               body.put ((byte) 0);
536             }
537
538             // LOG ("textclient thread finished %s (%d)", loading_url, size);
539             loaded_url_body = body;
540           }
541         }).start();
542
543       return null;
544     }
545   }
546
547
548   private ByteBuffer convertBitmap (String name, Bitmap bitmap,
549                                     int target_width, int target_height,
550                                     ExifInterface exif,
551                                     boolean rotate_p) {
552     if (bitmap == null) return null;
553
554     try {
555
556       int width  = bitmap.getWidth();
557       int height = bitmap.getHeight();
558
559       LOG ("read image %s: %d x %d", name, width, height);
560
561       // First rotate the image as per EXIF.
562
563       if (exif != null) {
564         int deg = 0;
565         switch (exif.getAttributeInt (ExifInterface.TAG_ORIENTATION,
566                                       ExifInterface.ORIENTATION_NORMAL)) {
567         case ExifInterface.ORIENTATION_ROTATE_90:  deg = 90;  break;
568         case ExifInterface.ORIENTATION_ROTATE_180: deg = 180; break;
569         case ExifInterface.ORIENTATION_ROTATE_270: deg = 270; break;
570         }
571         if (deg != 0) {
572           LOG ("%s: EXIF rotate %d", name, deg);
573           Matrix matrix = new Matrix();
574           matrix.preRotate (deg);
575           bitmap = Bitmap.createBitmap (bitmap, 0, 0, width, height,
576                                         matrix, true);
577           width  = bitmap.getWidth();
578           height = bitmap.getHeight();
579         }
580       }
581
582       // If the caller requested that we rotate the image to best fit the
583       // screen, rotate it again.  (Could combine this with the above and
584       // avoid copying the image again, but I'm lazy.)
585
586       if (rotate_p &&
587           (width > height) != (target_width > target_height)) {
588         LOG ("%s: rotated to fit screen", name);
589         Matrix matrix = new Matrix();
590         matrix.preRotate (90);
591         bitmap = Bitmap.createBitmap (bitmap, 0, 0, width, height,
592                                       matrix, true);
593         width  = bitmap.getWidth();
594         height = bitmap.getHeight();
595       }
596
597       // Resize the image to be not larger than the screen, potentially
598       // copying it for the third time.
599       // Actually, always scale it, scaling up if necessary.
600
601 //    if (width > target_width || height > target_height)
602       {
603         float r1 = target_width  / (float) width;
604         float r2 = target_height / (float) height;
605         float r = (r1 > r2 ? r2 : r1);
606         LOG ("%s: resize %.1f: %d x %d => %d x %d", name,
607              r, width, height, (int) (width * r), (int) (height * r));
608         width  = (int) (width * r);
609         height = (int) (height * r);
610         bitmap = Bitmap.createScaledBitmap (bitmap, width, height, true);
611         width  = bitmap.getWidth();
612         height = bitmap.getHeight();
613       }
614
615       // Now convert it to a ByteBuffer in the form expected by the C caller.
616
617       byte[] nameb = name.getBytes("UTF-8");
618       int size     = bitmap.getByteCount() + 4 + nameb.length + 1;
619
620       ByteBuffer bits = ByteBuffer.allocateDirect (size);
621
622       bits.put ((byte) ((width  >> 8) & 0xFF));
623       bits.put ((byte) ( width        & 0xFF));
624       bits.put ((byte) ((height >> 8) & 0xFF));
625       bits.put ((byte) ( height       & 0xFF));
626       bits.put (nameb);
627       bits.put ((byte) 0);
628
629       // The fourth of five copies.  Good thing these are supercomputers.
630       bitmap.copyPixelsToBuffer (bits);
631
632       return bits;
633
634     } catch (Exception e) {
635       LOG ("image %s unreadable: %s", name, e.toString());
636     }
637
638     return null;
639   }
640
641
642   public ByteBuffer loadRandomImage (int target_width, int target_height,
643                                      boolean rotate_p) {
644
645     int min_size = 480;
646     int max_size = 0x7FFF;
647
648     ArrayList<String> imgs = new ArrayList<String>();
649
650     ContentResolver cr = app.getContentResolver();
651     String[] cols = { MediaColumns.DATA,
652                       MediaColumns.MIME_TYPE,
653                       MediaColumns.WIDTH,
654                       MediaColumns.HEIGHT };
655     Uri uris[] = {
656       android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI,
657       android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI };
658
659     for (int i = 0; i < uris.length; i++) {
660       Cursor cursor = cr.query (uris[i], cols, null, null, null);
661       int j = 0;
662       int path_col   = cursor.getColumnIndexOrThrow (cols[j++]);
663       int type_col   = cursor.getColumnIndexOrThrow (cols[j++]);
664       int width_col  = cursor.getColumnIndexOrThrow (cols[j++]);
665       int height_col = cursor.getColumnIndexOrThrow (cols[j++]);
666       while (cursor.moveToNext()) {
667         String path = cursor.getString(path_col);
668         String type = cursor.getString(type_col);
669         int w = Integer.parseInt (cursor.getString(width_col));
670         int h = Integer.parseInt (cursor.getString(height_col));
671         if (type.startsWith("image/") &&
672             w > min_size && h > min_size &&
673             w < max_size && h < max_size) {
674           imgs.add (path);
675         }
676       }
677       cursor.close();
678     }
679
680     String which = null;
681
682     int count = imgs.size();
683     if (count == 0) {
684       LOG ("no images");
685       return null;
686     }
687
688     int i = new Random().nextInt (count);
689     which = imgs.get (i);
690     LOG ("picked image %d of %d: %s", i, count, which);
691
692     Uri uri = Uri.fromFile (new File (which));
693     String name = uri.getLastPathSegment();
694     Bitmap bitmap = null;
695     ExifInterface exif = null;
696
697     try {
698       bitmap = MediaStore.Images.Media.getBitmap (cr, uri);
699     } catch (Exception e) {
700       LOG ("image %s unloadable: %s", which, e.toString());
701       return null;
702     }
703
704     try {
705       exif = new ExifInterface (uri.getPath());  // If it fails, who cares
706     } catch (Exception e) {
707     }
708
709     ByteBuffer bits = convertBitmap (name, bitmap,
710                                      target_width, target_height,
711                                      exif, rotate_p);
712     bitmap.recycle();
713     return bits;
714   }
715
716
717   public ByteBuffer getScreenshot (int target_width, int target_height,
718                                    boolean rotate_p) {
719     return convertBitmap ("Screenshot", screenshot,
720                           target_width, target_height,
721                           null, rotate_p);
722   }
723
724
725   // Sadly duplicated from jwxyz.h (and thence X.h and keysymdef.h)
726   //
727   private static final int ShiftMask =     (1<<0);
728   private static final int LockMask =      (1<<1);
729   private static final int ControlMask =   (1<<2);
730   private static final int Mod1Mask =      (1<<3);
731   private static final int Mod2Mask =      (1<<4);
732   private static final int Mod3Mask =      (1<<5);
733   private static final int Mod4Mask =      (1<<6);
734   private static final int Mod5Mask =      (1<<7);
735   private static final int Button1Mask =   (1<<8);
736   private static final int Button2Mask =   (1<<9);
737   private static final int Button3Mask =   (1<<10);
738   private static final int Button4Mask =   (1<<11);
739   private static final int Button5Mask =   (1<<12);
740
741   private static final int XK_Shift_L =    0xFFE1;
742   private static final int XK_Shift_R =    0xFFE2;
743   private static final int XK_Control_L =  0xFFE3;
744   private static final int XK_Control_R =  0xFFE4;
745   private static final int XK_Caps_Lock =  0xFFE5;
746   private static final int XK_Shift_Lock = 0xFFE6;
747   private static final int XK_Meta_L =     0xFFE7;
748   private static final int XK_Meta_R =     0xFFE8;
749   private static final int XK_Alt_L =      0xFFE9;
750   private static final int XK_Alt_R =      0xFFEA;
751   private static final int XK_Super_L =    0xFFEB;
752   private static final int XK_Super_R =    0xFFEC;
753   private static final int XK_Hyper_L =    0xFFED;
754   private static final int XK_Hyper_R =    0xFFEE;
755
756   private static final int XK_Home =       0xFF50;
757   private static final int XK_Left =       0xFF51;
758   private static final int XK_Up =         0xFF52;
759   private static final int XK_Right =      0xFF53;
760   private static final int XK_Down =       0xFF54;
761   private static final int XK_Prior =      0xFF55;
762   private static final int XK_Page_Up =    0xFF55;
763   private static final int XK_Next =       0xFF56;
764   private static final int XK_Page_Down =  0xFF56;
765   private static final int XK_End =        0xFF57;
766   private static final int XK_Begin =      0xFF58;
767
768   private static final int XK_F1 =         0xFFBE;
769   private static final int XK_F2 =         0xFFBF;
770   private static final int XK_F3 =         0xFFC0;
771   private static final int XK_F4 =         0xFFC1;
772   private static final int XK_F5 =         0xFFC2;
773   private static final int XK_F6 =         0xFFC3;
774   private static final int XK_F7 =         0xFFC4;
775   private static final int XK_F8 =         0xFFC5;
776   private static final int XK_F9 =         0xFFC6;
777   private static final int XK_F10 =        0xFFC7;
778   private static final int XK_F11 =        0xFFC8;
779   private static final int XK_F12 =        0xFFC9;
780
781   public void sendKeyEvent (KeyEvent event) {
782     int uc    = event.getUnicodeChar();
783     int jcode = event.getKeyCode();
784     int jmods = event.getModifiers();
785     int xcode = 0;
786     int xmods = 0;
787
788     switch (jcode) {
789     case KeyEvent.KEYCODE_SHIFT_LEFT:        xcode = XK_Shift_L;   break;
790     case KeyEvent.KEYCODE_SHIFT_RIGHT:       xcode = XK_Shift_R;   break;
791     case KeyEvent.KEYCODE_CTRL_LEFT:         xcode = XK_Control_L; break;
792     case KeyEvent.KEYCODE_CTRL_RIGHT:        xcode = XK_Control_R; break;
793     case KeyEvent.KEYCODE_CAPS_LOCK:         xcode = XK_Caps_Lock; break;
794     case KeyEvent.KEYCODE_META_LEFT:         xcode = XK_Meta_L;    break;
795     case KeyEvent.KEYCODE_META_RIGHT:        xcode = XK_Meta_R;    break;
796     case KeyEvent.KEYCODE_ALT_LEFT:          xcode = XK_Alt_L;     break;
797     case KeyEvent.KEYCODE_ALT_RIGHT:         xcode = XK_Alt_R;     break;
798
799     case KeyEvent.KEYCODE_HOME:              xcode = XK_Home;      break;
800     case KeyEvent.KEYCODE_DPAD_LEFT:         xcode = XK_Left;      break;
801     case KeyEvent.KEYCODE_DPAD_UP:           xcode = XK_Up;        break;
802     case KeyEvent.KEYCODE_DPAD_RIGHT:        xcode = XK_Right;     break;
803     case KeyEvent.KEYCODE_DPAD_DOWN:         xcode = XK_Down;      break;
804   //case KeyEvent.KEYCODE_NAVIGATE_PREVIOUS: xcode = XK_Prior;     break;
805     case KeyEvent.KEYCODE_PAGE_UP:           xcode = XK_Page_Up;   break;
806   //case KeyEvent.KEYCODE_NAVIGATE_NEXT:     xcode = XK_Next;      break;
807     case KeyEvent.KEYCODE_PAGE_DOWN:         xcode = XK_Page_Down; break;
808     case KeyEvent.KEYCODE_MOVE_END:          xcode = XK_End;       break;
809     case KeyEvent.KEYCODE_MOVE_HOME:         xcode = XK_Begin;     break;
810
811     case KeyEvent.KEYCODE_F1:                xcode = XK_F1;        break;
812     case KeyEvent.KEYCODE_F2:                xcode = XK_F2;        break;
813     case KeyEvent.KEYCODE_F3:                xcode = XK_F3;        break;
814     case KeyEvent.KEYCODE_F4:                xcode = XK_F4;        break;
815     case KeyEvent.KEYCODE_F5:                xcode = XK_F5;        break;
816     case KeyEvent.KEYCODE_F6:                xcode = XK_F6;        break;
817     case KeyEvent.KEYCODE_F7:                xcode = XK_F7;        break;
818     case KeyEvent.KEYCODE_F8:                xcode = XK_F8;        break;
819     case KeyEvent.KEYCODE_F9:                xcode = XK_F9;        break;
820     case KeyEvent.KEYCODE_F10:               xcode = XK_F10;       break;
821     case KeyEvent.KEYCODE_F11:               xcode = XK_F11;       break;
822     case KeyEvent.KEYCODE_F12:               xcode = XK_F12;       break;
823     default:                                 xcode = uc;           break;
824     }
825
826     if (0 != (jmods & KeyEvent.META_SHIFT_ON))     xmods |= ShiftMask;
827     if (0 != (jmods & KeyEvent.META_CAPS_LOCK_ON)) xmods |= LockMask;
828     if (0 != (jmods & KeyEvent.META_CTRL_MASK))    xmods |= ControlMask;
829     if (0 != (jmods & KeyEvent.META_ALT_MASK))     xmods |= Mod1Mask;
830     if (0 != (jmods & KeyEvent.META_META_ON))      xmods |= Mod1Mask;
831     if (0 != (jmods & KeyEvent.META_SYM_ON))       xmods |= Mod2Mask;
832     if (0 != (jmods & KeyEvent.META_FUNCTION_ON))  xmods |= Mod3Mask;
833
834     /* If you touch and release Shift, you get no events.
835        If you type Shift-A, you get Shift down, A down, A up, Shift up.
836        So let's just ignore all lone modifier key events.
837      */
838     if (xcode >= XK_Shift_L && xcode <= XK_Hyper_R)
839       return;
840
841     boolean down_p = event.getAction() == KeyEvent.ACTION_DOWN;
842     sendKeyEvent (down_p, xcode, xmods);
843   }
844
845 }