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