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