1 /* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * xscreensaver, Copyright (c) 2016-2018 Jamie Zawinski <jwz@jwz.org>
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
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.
17 package org.jwz.xscreensaver;
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;
45 import java.nio.ByteBuffer;
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;
67 implements GestureDetector.OnGestureListener,
68 GestureDetector.OnDoubleTapListener {
70 private class PrefListener
71 implements SharedPreferences.OnSharedPreferenceChangeListener {
74 public void onSharedPreferenceChanged (SharedPreferences sharedPreferences, String key)
76 if (key.startsWith(hack + "_")) {
78 boolean was_animating;
79 synchronized (render) {
80 was_animating = animating_p;
90 private static class SurfaceLost extends Exception {
92 super("surface lost");
95 SurfaceLost (String detailMessage) {
100 public final static int STYLE_BOLD = 1;
101 public final static int STYLE_ITALIC = 2;
102 public final static int STYLE_MONOSPACE = 4;
104 public final static int FONT_FAMILY = 0;
105 public final static int FONT_FACE = 1;
106 public final static int FONT_RANDOM = 2;
108 public final static int MY_REQ_READ_EXTERNAL_STORAGE = 271828;
110 private long nativeRunningHackPtr;
114 private Bitmap screenshot;
116 SharedPreferences prefs;
117 SharedPreferences.OnSharedPreferenceChangeListener pref_listener;
118 Hashtable<String, String> defaults = new Hashtable<String, String>();
121 // Maps font names to either: String (system font) or Typeface (bundled).
122 private Hashtable<String, Object> all_fonts =
123 new Hashtable<String, Object>();
129 // Doubles as the mutex controlling width/height/animating_p.
130 private Thread render;
132 private Runnable on_quit;
133 boolean button_down_p;
135 // These are defined in jwxyz-android.c:
137 private native long nativeInit (String hack,
138 Hashtable<String,String> defaults,
139 int w, int h, Surface window)
141 private native void nativeResize (int w, int h, double rot);
142 private native long nativeRender ();
143 private native void nativeDone ();
144 public native void sendButtonEvent (int x, int y, boolean down);
145 public native void sendMotionEvent (int x, int y);
146 public native void sendKeyEvent (boolean down_p, int code, int mods);
148 private void LOG (String fmt, Object... args) {
149 Log.d ("xscreensaver", hack + ": " + String.format (fmt, args));
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('$');
158 name = name.substring (index, name.length() - index);
160 return name.toLowerCase();
164 public jwxyz (String hack, Context app, Bitmap screenshot, int w, int h,
165 Surface surface, Runnable on_quit) {
169 this.screenshot = screenshot;
170 this.on_quit = on_quit;
173 this.surface = surface;
175 // nativeInit populates 'defaults' with the default values for keys
176 // that are not overridden by SharedPreferences.
178 prefs = app.getSharedPreferences (hack, 0);
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);
188 protected void finalize() {
189 if (render != null) {
190 LOG ("jwxyz finalized without close. This might be OK.");
196 public String getStringResource (String name) {
198 name = hack + "_" + name;
200 if (prefs.contains(name)) {
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.
207 try { return prefs.getString (name, "");
208 } catch (Exception e) { }
210 try { return Float.toString (prefs.getFloat (name, 0));
211 } catch (Exception e) { }
213 try { return Long.toString (prefs.getLong (name, 0));
214 } catch (Exception e) { }
216 try { return Integer.toString (prefs.getInt (name, 0));
217 } catch (Exception e) { }
219 try { return (prefs.getBoolean (name, false) ? "true" : "false");
220 } catch (Exception e) { }
223 // If we got to here, it's not in there, so return the default.
224 return defaults.get (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);
243 private void scanSystemFonts() {
245 // First parse the system font directories for the global fonts.
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);
253 File[] files = dir.listFiles();
257 for (File file : files) {
258 String name = analyzer.getTtfFontName (file.getAbsolutePath());
260 // LOG ("unparsable system font: %s", file);
262 name = mungeFontName (name);
263 if (! all_fonts.contains (name)) {
264 // LOG ("system font \"%s\" %s", name, file);
265 all_fonts.put (name, name);
271 // Now parse our assets, for our bundled fonts.
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()); }
279 for (String fn : files) {
280 String fn2 = dir + "/" + fn;
281 Typeface t = Typeface.createFromAsset (am, fn2);
285 tmpfile = new File(app.getCacheDir(), fn);
286 if (tmpfile.createNewFile() == false) {
288 tmpfile.createNewFile();
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) {
300 String name = analyzer.getTtfFontName (tmpfile.getAbsolutePath());
303 name = mungeFontName (name);
304 all_fonts.put (name, t);
305 // LOG ("asset font \"%s\" %s", name, fn);
306 } catch (Exception e) {
307 if (tmpfile != null) tmpfile.delete();
308 LOG ("error: %s", e.toString());
314 // Parses family names from X Logical Font Descriptions, including a few
315 // standard X font names that aren't handled by try_xlfd_font().
316 // Returns [ String name, Typeface ]
317 private Object[] parseXLFD (int mask, int traits,
318 String name, int name_type) {
319 boolean fixed = false;
320 boolean serif = false;
322 int style_jwxyz = mask & traits;
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")) {
332 } else if (name.equals("times") ||
333 name.equals("georgia") ||
334 name.equals("serif")) {
336 } else if (name.equals("serif-monospace")) {
341 Random r = new Random();
342 serif = r.nextBoolean(); // Not much to randomize here...
343 fixed = (r.nextInt(8) == 0);
347 ? (serif ? "serif-monospace" : "monospace")
348 : (serif ? "serif" : "sans-serif"));
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;
356 return new Object[] { name, Typeface.create(name, style_android) };
360 // Parses "Native Font Name One 12, Native Font Name Two 14".
361 // Returns [ String name, Typeface ]
362 private Object[] parseNativeFont (String name) {
363 Object font2 = all_fonts.get (name);
364 if (font2 instanceof String)
365 font2 = Typeface.create (name, Typeface.NORMAL);
366 return new Object[] { name, (Typeface)font2 };
370 // Returns [ Paint paint, String family_name, Float ascent, Float descent ]
371 public Object[] loadFont(int mask, int traits, String name, int name_type,
375 if (name_type != FONT_RANDOM && name.equals("")) return null;
377 if (name_type == FONT_FACE) {
378 pair = parseNativeFont (name);
380 pair = parseXLFD (mask, traits, name, name_type);
383 String name2 = (String) pair[0];
384 Typeface font = (Typeface) pair[1];
388 String suffix = (font.isBold() && font.isItalic() ? " bold italic" :
389 font.isBold() ? " bold" :
390 font.isItalic() ? " italic" :
392 Paint paint = new Paint();
393 paint.setTypeface (font);
394 paint.setTextSize (size);
395 paint.setColor (Color.argb (0xFF, 0xFF, 0xFF, 0xFF));
397 LOG ("load font \"%s\" = \"%s %.1f\"", name, name2 + suffix, size);
399 FontMetrics fm = paint.getFontMetrics();
400 return new Object[] { paint, name2, -fm.ascent, fm.descent };
404 /* Returns a byte[] array containing XCharStruct with an optional
405 bitmap appended to it.
406 lbearing, rbearing, width, ascent, descent: 2 bytes each.
407 Followed by a WxH pixmap, 32 bits per pixel.
409 public ByteBuffer renderText (Paint paint, String text, boolean render_p,
410 boolean antialias_p) {
417 /* Font metric terminology, as used by X11:
419 "lbearing" is the distance from the logical origin to the leftmost
420 pixel. If a character's ink extends to the left of the origin, it is
423 "rbearing" is the distance from the logical origin to the rightmost
426 "descent" is the distance from the logical origin to the bottommost
427 pixel. For characters with descenders, it is positive. For
428 superscripts, it is negative.
430 "ascent" is the distance from the logical origin to the topmost pixel.
431 It is the number of pixels above the baseline.
433 "width" is the distance from the logical origin to the position where
434 the logical origin of the next character should be placed.
436 If "rbearing" is greater than "width", then this character overlaps the
437 following character. If smaller, then there is trailing blank space.
439 The bbox coordinates returned by getTextBounds grow down and right:
440 for a character with ink both above and below the baseline, top is
441 negative and bottom is positive.
443 paint.setAntiAlias (antialias_p);
444 FontMetrics fm = paint.getFontMetrics();
445 Rect bbox = new Rect();
446 paint.getTextBounds (text, 0, text.length(), bbox);
448 /* The bbox returned by getTextBounds measures from the logical origin
449 with right and down being positive. This means most characters have
450 a negative top, and characters with descenders have a positive bottom.
452 int lbearing = bbox.left;
453 int rbearing = bbox.right;
454 int ascent = -bbox.top;
455 int descent = bbox.bottom;
456 int width = (int) paint.measureText (text);
458 int w = rbearing - lbearing;
459 int h = ascent + descent;
460 int size = 5 * 2 + (render_p ? w * h * 4 : 0);
462 ByteBuffer bits = ByteBuffer.allocateDirect (size);
464 bits.put ((byte) ((lbearing >> 8) & 0xFF));
465 bits.put ((byte) ( lbearing & 0xFF));
466 bits.put ((byte) ((rbearing >> 8) & 0xFF));
467 bits.put ((byte) ( rbearing & 0xFF));
468 bits.put ((byte) ((width >> 8) & 0xFF));
469 bits.put ((byte) ( width & 0xFF));
470 bits.put ((byte) ((ascent >> 8) & 0xFF));
471 bits.put ((byte) ( ascent & 0xFF));
472 bits.put ((byte) ((descent >> 8) & 0xFF));
473 bits.put ((byte) ( descent & 0xFF));
475 if (render_p && w > 0 && h > 0) {
476 Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
477 Canvas canvas = new Canvas (bitmap);
478 canvas.drawText (text, -lbearing, ascent, paint);
479 bitmap.copyPixelsToBuffer (bits);
487 /* Returns the contents of the URL.
488 Loads the URL in a background thread: if the URL has not yet loaded,
489 this will return null. Once the URL has completely loaded, the full
490 contents will be returned. Calling this again after that starts the
493 private String loading_url = null;
494 private ByteBuffer loaded_url_body = null;
496 public synchronized ByteBuffer loadURL (String url) {
498 if (loaded_url_body != null) { // Thread finished
500 // LOG ("textclient finished %s", loading_url);
502 ByteBuffer bb = loaded_url_body;
504 loaded_url_body = null;
507 } else if (loading_url != null) { // Waiting on thread
508 // LOG ("textclient waiting...");
511 } else { // Launch thread
514 LOG ("textclient launching %s...", url);
516 new Thread (new Runnable() {
521 ByteBuffer body = ByteBuffer.allocateDirect (size);
524 URL u = new URL (loading_url);
525 // LOG ("textclient thread loading: %s", u.toString());
526 InputStream s = u.openStream();
527 byte buf[] = new byte[10240];
529 int n = s.read (buf);
531 // LOG ("textclient thread read %d", n);
532 if (count + n + 1 >= size) {
533 int size2 = (int) (size * 1.2 + size0);
534 // LOG ("textclient thread expand %d -> %d", size, size2);
535 ByteBuffer body2 = ByteBuffer.allocateDirect (size2);
538 body2.position (count);
542 body.put (buf, 0, n);
545 } catch (Exception e) {
546 LOG ("load URL error: %s", e.toString());
548 body.put (e.toString().getBytes());
552 // LOG ("textclient thread finished %s (%d)", loading_url, size);
553 loaded_url_body = body;
562 // Returns [ Bitmap bitmap, String name ]
563 private Object[] convertBitmap (String name, Bitmap bitmap,
564 int target_width, int target_height,
565 ExifInterface exif, boolean rotate_p) {
566 if (bitmap == null) return null;
570 int width = bitmap.getWidth();
571 int height = bitmap.getHeight();
572 Matrix matrix = new Matrix();
574 LOG ("read image %s: %d x %d", name, width, height);
576 // First rotate the image as per EXIF.
580 switch (exif.getAttributeInt (ExifInterface.TAG_ORIENTATION,
581 ExifInterface.ORIENTATION_NORMAL)) {
582 case ExifInterface.ORIENTATION_ROTATE_90: deg = 90; break;
583 case ExifInterface.ORIENTATION_ROTATE_180: deg = 180; break;
584 case ExifInterface.ORIENTATION_ROTATE_270: deg = 270; break;
587 LOG ("%s: EXIF rotate %d", name, deg);
588 matrix.preRotate (deg);
589 if (deg == 90 || deg == 270) {
597 // If the caller requested that we rotate the image to best fit the
598 // screen, rotate it again.
601 (width > height) != (target_width > target_height)) {
602 LOG ("%s: rotated to fit screen", name);
603 matrix.preRotate (90);
610 // Resize the image to be not larger than the screen, potentially
611 // copying it for the third time.
612 // Actually, always scale it, scaling up if necessary.
614 // if (width > target_width || height > target_height)
616 float r1 = target_width / (float) width;
617 float r2 = target_height / (float) height;
618 float r = (r1 > r2 ? r2 : r1);
619 LOG ("%s: resize %.1f: %d x %d => %d x %d", name,
620 r, width, height, (int) (width * r), (int) (height * r));
621 matrix.preScale (r, r);
624 bitmap = Bitmap.createBitmap (bitmap, 0, 0,
625 bitmap.getWidth(), bitmap.getHeight(),
628 if (bitmap.getConfig() != Bitmap.Config.ARGB_8888)
629 bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false);
631 return new Object[] { bitmap, name };
637 boolean havePermission(String permission) {
639 if (Build.VERSION.SDK_INT < 16) {
643 if (permissionGranted(permission)) {
651 private boolean permissionGranted(String permission) {
652 boolean check = ContextCompat.checkSelfPermission(app, permission) ==
653 PackageManager.PERMISSION_GRANTED;
657 public Object[] checkThenLoadRandomImage (int target_width, int target_height,
659 // RES introduced in API 16
660 String permission = Manifest.permission.READ_EXTERNAL_STORAGE;
662 if (havePermission(permission)) {
663 return loadRandomImage(target_width,target_height,rotate_p);
669 public Object[] loadRandomImage (int target_width, int target_height,
673 int max_size = 0x7FFF;
675 ArrayList<String> imgs = new ArrayList<String>();
677 ContentResolver cr = app.getContentResolver();
678 String[] cols = { MediaColumns.DATA,
679 MediaColumns.MIME_TYPE,
681 MediaColumns.HEIGHT };
683 android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI,
684 android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI };
686 for (int i = 0; i < uris.length; i++) {
687 Cursor cursor = cr.query (uris[i], cols, null, null, null);
691 int path_col = cursor.getColumnIndexOrThrow (cols[j++]);
692 int type_col = cursor.getColumnIndexOrThrow (cols[j++]);
693 int width_col = cursor.getColumnIndexOrThrow (cols[j++]);
694 int height_col = cursor.getColumnIndexOrThrow (cols[j++]);
695 while (cursor.moveToNext()) {
696 String path = cursor.getString(path_col);
697 String type = cursor.getString(type_col);
698 if (path != null && type != null && type.startsWith("image/")) {
699 String wc = cursor.getString(width_col);
700 String hc = cursor.getString(height_col);
701 if (wc != null && hc != null) {
702 int w = Integer.parseInt (wc);
703 int h = Integer.parseInt (hc);
704 if (w > min_size && h > min_size &&
705 w < max_size && h < max_size) {
716 int count = imgs.size();
722 int i = new Random().nextInt (count);
723 which = imgs.get (i);
724 LOG ("picked image %d of %d: %s", i, count, which);
726 Uri uri = Uri.fromFile (new File (which));
727 String name = uri.getLastPathSegment();
728 Bitmap bitmap = null;
729 ExifInterface exif = null;
733 bitmap = MediaStore.Images.Media.getBitmap (cr, uri);
734 } catch (Exception e) {
735 LOG ("image %s unloadable: %s", which, e.toString());
740 exif = new ExifInterface (uri.getPath()); // If it fails, who cares
741 } catch (Exception e) {
744 return convertBitmap (name, bitmap, target_width, target_height,
746 } catch (java.lang.OutOfMemoryError e) {
747 LOG ("image %s got OutOfMemoryError: %s", which, e.toString());
753 public Object[] getScreenshot (int target_width, int target_height,
755 return convertBitmap ("Screenshot", screenshot,
756 target_width, target_height,
761 public Bitmap decodePNG (byte[] data) {
762 BitmapFactory.Options opts = new BitmapFactory.Options();
763 opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
764 return BitmapFactory.decodeByteArray (data, 0, data.length, opts);
768 // Sadly duplicated from jwxyz.h (and thence X.h and keysymdef.h)
770 private static final int ShiftMask = (1<<0);
771 private static final int LockMask = (1<<1);
772 private static final int ControlMask = (1<<2);
773 private static final int Mod1Mask = (1<<3);
774 private static final int Mod2Mask = (1<<4);
775 private static final int Mod3Mask = (1<<5);
776 private static final int Mod4Mask = (1<<6);
777 private static final int Mod5Mask = (1<<7);
778 private static final int Button1Mask = (1<<8);
779 private static final int Button2Mask = (1<<9);
780 private static final int Button3Mask = (1<<10);
781 private static final int Button4Mask = (1<<11);
782 private static final int Button5Mask = (1<<12);
784 private static final int XK_Shift_L = 0xFFE1;
785 private static final int XK_Shift_R = 0xFFE2;
786 private static final int XK_Control_L = 0xFFE3;
787 private static final int XK_Control_R = 0xFFE4;
788 private static final int XK_Caps_Lock = 0xFFE5;
789 private static final int XK_Shift_Lock = 0xFFE6;
790 private static final int XK_Meta_L = 0xFFE7;
791 private static final int XK_Meta_R = 0xFFE8;
792 private static final int XK_Alt_L = 0xFFE9;
793 private static final int XK_Alt_R = 0xFFEA;
794 private static final int XK_Super_L = 0xFFEB;
795 private static final int XK_Super_R = 0xFFEC;
796 private static final int XK_Hyper_L = 0xFFED;
797 private static final int XK_Hyper_R = 0xFFEE;
799 private static final int XK_Home = 0xFF50;
800 private static final int XK_Left = 0xFF51;
801 private static final int XK_Up = 0xFF52;
802 private static final int XK_Right = 0xFF53;
803 private static final int XK_Down = 0xFF54;
804 private static final int XK_Prior = 0xFF55;
805 private static final int XK_Page_Up = 0xFF55;
806 private static final int XK_Next = 0xFF56;
807 private static final int XK_Page_Down = 0xFF56;
808 private static final int XK_End = 0xFF57;
809 private static final int XK_Begin = 0xFF58;
811 private static final int XK_F1 = 0xFFBE;
812 private static final int XK_F2 = 0xFFBF;
813 private static final int XK_F3 = 0xFFC0;
814 private static final int XK_F4 = 0xFFC1;
815 private static final int XK_F5 = 0xFFC2;
816 private static final int XK_F6 = 0xFFC3;
817 private static final int XK_F7 = 0xFFC4;
818 private static final int XK_F8 = 0xFFC5;
819 private static final int XK_F9 = 0xFFC6;
820 private static final int XK_F10 = 0xFFC7;
821 private static final int XK_F11 = 0xFFC8;
822 private static final int XK_F12 = 0xFFC9;
824 public void sendKeyEvent (KeyEvent event) {
825 int uc = event.getUnicodeChar();
826 int jcode = event.getKeyCode();
827 int jmods = event.getModifiers();
832 case KeyEvent.KEYCODE_SHIFT_LEFT: xcode = XK_Shift_L; break;
833 case KeyEvent.KEYCODE_SHIFT_RIGHT: xcode = XK_Shift_R; break;
834 case KeyEvent.KEYCODE_CTRL_LEFT: xcode = XK_Control_L; break;
835 case KeyEvent.KEYCODE_CTRL_RIGHT: xcode = XK_Control_R; break;
836 case KeyEvent.KEYCODE_CAPS_LOCK: xcode = XK_Caps_Lock; break;
837 case KeyEvent.KEYCODE_META_LEFT: xcode = XK_Meta_L; break;
838 case KeyEvent.KEYCODE_META_RIGHT: xcode = XK_Meta_R; break;
839 case KeyEvent.KEYCODE_ALT_LEFT: xcode = XK_Alt_L; break;
840 case KeyEvent.KEYCODE_ALT_RIGHT: xcode = XK_Alt_R; break;
842 case KeyEvent.KEYCODE_HOME: xcode = XK_Home; break;
843 case KeyEvent.KEYCODE_DPAD_LEFT: xcode = XK_Left; break;
844 case KeyEvent.KEYCODE_DPAD_UP: xcode = XK_Up; break;
845 case KeyEvent.KEYCODE_DPAD_RIGHT: xcode = XK_Right; break;
846 case KeyEvent.KEYCODE_DPAD_DOWN: xcode = XK_Down; break;
847 //case KeyEvent.KEYCODE_NAVIGATE_PREVIOUS: xcode = XK_Prior; break;
848 case KeyEvent.KEYCODE_PAGE_UP: xcode = XK_Page_Up; break;
849 //case KeyEvent.KEYCODE_NAVIGATE_NEXT: xcode = XK_Next; break;
850 case KeyEvent.KEYCODE_PAGE_DOWN: xcode = XK_Page_Down; break;
851 case KeyEvent.KEYCODE_MOVE_END: xcode = XK_End; break;
852 case KeyEvent.KEYCODE_MOVE_HOME: xcode = XK_Begin; break;
854 case KeyEvent.KEYCODE_F1: xcode = XK_F1; break;
855 case KeyEvent.KEYCODE_F2: xcode = XK_F2; break;
856 case KeyEvent.KEYCODE_F3: xcode = XK_F3; break;
857 case KeyEvent.KEYCODE_F4: xcode = XK_F4; break;
858 case KeyEvent.KEYCODE_F5: xcode = XK_F5; break;
859 case KeyEvent.KEYCODE_F6: xcode = XK_F6; break;
860 case KeyEvent.KEYCODE_F7: xcode = XK_F7; break;
861 case KeyEvent.KEYCODE_F8: xcode = XK_F8; break;
862 case KeyEvent.KEYCODE_F9: xcode = XK_F9; break;
863 case KeyEvent.KEYCODE_F10: xcode = XK_F10; break;
864 case KeyEvent.KEYCODE_F11: xcode = XK_F11; break;
865 case KeyEvent.KEYCODE_F12: xcode = XK_F12; break;
866 default: xcode = uc; break;
869 if (0 != (jmods & KeyEvent.META_SHIFT_ON)) xmods |= ShiftMask;
870 if (0 != (jmods & KeyEvent.META_CAPS_LOCK_ON)) xmods |= LockMask;
871 if (0 != (jmods & KeyEvent.META_CTRL_MASK)) xmods |= ControlMask;
872 if (0 != (jmods & KeyEvent.META_ALT_MASK)) xmods |= Mod1Mask;
873 if (0 != (jmods & KeyEvent.META_META_ON)) xmods |= Mod1Mask;
874 if (0 != (jmods & KeyEvent.META_SYM_ON)) xmods |= Mod2Mask;
875 if (0 != (jmods & KeyEvent.META_FUNCTION_ON)) xmods |= Mod3Mask;
877 /* If you touch and release Shift, you get no events.
878 If you type Shift-A, you get Shift down, A down, A up, Shift up.
879 So let's just ignore all lone modifier key events.
881 if (xcode >= XK_Shift_L && xcode <= XK_Hyper_R)
884 boolean down_p = event.getAction() == KeyEvent.ACTION_DOWN;
885 sendKeyEvent (down_p, xcode, xmods);
889 if (render == null) {
891 render = new Thread(new Runnable() {
895 int currentWidth, currentHeight;
896 synchronized (render) {
898 while (!animating_p || width == 0 || height == 0) {
901 } catch(InterruptedException exc) {
907 nativeInit (hack, defaults, width, height, surface);
908 currentWidth = width;
909 currentHeight= height;
911 } catch (SurfaceLost exc) {
920 synchronized (render) {
923 while (!animating_p) {
926 } catch(InterruptedException exc) {
931 if (currentWidth != width || currentHeight != height) {
932 currentWidth = width;
933 currentHeight = height;
934 nativeResize (width, height, 0);
938 long delay = nativeRender();
940 synchronized (render) {
943 render.wait(delay / 1000, (int)(delay % 1000) * 1000);
944 } catch (InterruptedException exc) {
948 if (Thread.interrupted ()) {
955 assert nativeRunningHackPtr != 0;
962 synchronized(render) {
972 synchronized (render) {
981 synchronized (render) {
987 } catch (InterruptedException exc) {
992 void resize (int w, int h) {
995 if (render != null) {
996 synchronized (render) {
1008 /* We distinguish between taps and drags.
1010 - Drags/pans (down, motion, up) are sent to the saver to handle.
1011 - Single-taps exit the saver.
1012 - Long-press single-taps are sent to the saver as ButtonPress/Release;
1013 - Double-taps are sent to the saver as a "Space" keypress.
1016 - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow.
1020 public boolean onSingleTapConfirmed (MotionEvent event) {
1021 if (on_quit != null)
1027 public boolean onDoubleTap (MotionEvent event) {
1028 sendKeyEvent (new KeyEvent (KeyEvent.ACTION_DOWN,
1029 KeyEvent.KEYCODE_SPACE));
1034 public void onLongPress (MotionEvent event) {
1035 if (! button_down_p) {
1036 int x = (int) event.getX (event.getPointerId (0));
1037 int y = (int) event.getY (event.getPointerId (0));
1038 sendButtonEvent (x, y, true);
1039 sendButtonEvent (x, y, false);
1044 public void onShowPress (MotionEvent event) {
1045 if (! button_down_p) {
1046 button_down_p = true;
1047 int x = (int) event.getX (event.getPointerId (0));
1048 int y = (int) event.getY (event.getPointerId (0));
1049 sendButtonEvent (x, y, true);
1054 public boolean onScroll (MotionEvent e1, MotionEvent e2,
1055 float distanceX, float distanceY) {
1056 // LOG ("onScroll: %d", button_down_p ? 1 : 0);
1058 sendMotionEvent ((int) e2.getX (e2.getPointerId (0)),
1059 (int) e2.getY (e2.getPointerId (0)));
1063 // If you drag too fast, you get a single onFling event instead of a
1064 // succession of onScroll events. I can't figure out how to disable it.
1066 public boolean onFling (MotionEvent e1, MotionEvent e2,
1067 float velocityX, float velocityY) {
1071 public boolean dragEnded (MotionEvent event) {
1072 if (button_down_p) {
1073 int x = (int) event.getX (event.getPointerId (0));
1074 int y = (int) event.getY (event.getPointerId (0));
1075 sendButtonEvent (x, y, false);
1076 button_down_p = false;
1082 public boolean onDown (MotionEvent event) {
1087 public boolean onSingleTapUp (MotionEvent event) {
1092 public boolean onDoubleTapEvent (MotionEvent event) {
1098 System.loadLibrary ("xscreensaver");
1101 Thread.setDefaultUncaughtExceptionHandler(
1102 new Thread.UncaughtExceptionHandler() {
1103 Thread.UncaughtExceptionHandler old_handler =
1104 Thread.currentThread().getUncaughtExceptionHandler();
1107 public void uncaughtException (Thread thread, Throwable ex) {
1108 String err = ex.toString();
1109 Log.d ("xscreensaver", "Caught exception: " + err);
1110 old_handler.uncaughtException (thread, ex);