1 package com.android.launcher3; 2 3 import android.appwidget.AppWidgetProviderInfo; 4 import android.content.ComponentName; 5 import android.content.ContentValues; 6 import android.content.Context; 7 import android.content.SharedPreferences; 8 import android.content.pm.ResolveInfo; 9 import android.content.res.Resources; 10 import android.database.Cursor; 11 import android.database.sqlite.SQLiteCantOpenDatabaseException; 12 import android.database.sqlite.SQLiteDatabase; 13 import android.database.sqlite.SQLiteDiskIOException; 14 import android.database.sqlite.SQLiteOpenHelper; 15 import android.graphics.Bitmap; 16 import android.graphics.Bitmap.Config; 17 import android.graphics.BitmapFactory; 18 import android.graphics.BitmapShader; 19 import android.graphics.Canvas; 20 import android.graphics.ColorMatrix; 21 import android.graphics.ColorMatrixColorFilter; 22 import android.graphics.Paint; 23 import android.graphics.PorterDuff; 24 import android.graphics.Rect; 25 import android.graphics.Shader; 26 import android.graphics.drawable.BitmapDrawable; 27 import android.graphics.drawable.Drawable; 28 import android.os.AsyncTask; 29 import android.util.Log; 30 31 import com.android.launcher3.compat.AppWidgetManagerCompat; 32 33 import java.io.ByteArrayOutputStream; 34 import java.io.File; 35 import java.io.IOException; 36 import java.lang.ref.SoftReference; 37 import java.lang.ref.WeakReference; 38 import java.util.ArrayList; 39 import java.util.Arrays; 40 import java.util.HashMap; 41 import java.util.HashSet; 42 import java.util.List; 43 import java.util.concurrent.Callable; 44 import java.util.concurrent.ExecutionException; 45 46 public class WidgetPreviewLoader { 47 48 private static abstract class SoftReferenceThreadLocal<T> { 49 private ThreadLocal<SoftReference<T>> mThreadLocal; 50 public SoftReferenceThreadLocal() { 51 mThreadLocal = new ThreadLocal<SoftReference<T>>(); 52 } 53 54 abstract T initialValue(); 55 56 public void set(T t) { 57 mThreadLocal.set(new SoftReference<T>(t)); 58 } 59 60 public T get() { 61 SoftReference<T> reference = mThreadLocal.get(); 62 T obj; 63 if (reference == null) { 64 obj = initialValue(); 65 mThreadLocal.set(new SoftReference<T>(obj)); 66 return obj; 67 } else { 68 obj = reference.get(); 69 if (obj == null) { 70 obj = initialValue(); 71 mThreadLocal.set(new SoftReference<T>(obj)); 72 } 73 return obj; 74 } 75 } 76 } 77 78 private static class CanvasCache extends SoftReferenceThreadLocal<Canvas> { 79 @Override 80 protected Canvas initialValue() { 81 return new Canvas(); 82 } 83 } 84 85 private static class PaintCache extends SoftReferenceThreadLocal<Paint> { 86 @Override 87 protected Paint initialValue() { 88 return null; 89 } 90 } 91 92 private static class BitmapCache extends SoftReferenceThreadLocal<Bitmap> { 93 @Override 94 protected Bitmap initialValue() { 95 return null; 96 } 97 } 98 99 private static class RectCache extends SoftReferenceThreadLocal<Rect> { 100 @Override 101 protected Rect initialValue() { 102 return new Rect(); 103 } 104 } 105 106 private static class BitmapFactoryOptionsCache extends 107 SoftReferenceThreadLocal<BitmapFactory.Options> { 108 @Override 109 protected BitmapFactory.Options initialValue() { 110 return new BitmapFactory.Options(); 111 } 112 } 113 114 private static final String TAG = "WidgetPreviewLoader"; 115 private static final String ANDROID_INCREMENTAL_VERSION_NAME_KEY = "android.incremental.version"; 116 117 private static final float WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE = 0.25f; 118 private static final HashSet<String> sInvalidPackages = new HashSet<String>(); 119 120 // Used for drawing shortcut previews 121 private final BitmapCache mCachedShortcutPreviewBitmap = new BitmapCache(); 122 private final PaintCache mCachedShortcutPreviewPaint = new PaintCache(); 123 private final CanvasCache mCachedShortcutPreviewCanvas = new CanvasCache(); 124 125 // Used for drawing widget previews 126 private final CanvasCache mCachedAppWidgetPreviewCanvas = new CanvasCache(); 127 private final RectCache mCachedAppWidgetPreviewSrcRect = new RectCache(); 128 private final RectCache mCachedAppWidgetPreviewDestRect = new RectCache(); 129 private final PaintCache mCachedAppWidgetPreviewPaint = new PaintCache(); 130 private final PaintCache mDefaultAppWidgetPreviewPaint = new PaintCache(); 131 private final BitmapFactoryOptionsCache mCachedBitmapFactoryOptions = new BitmapFactoryOptionsCache(); 132 133 private final HashMap<String, WeakReference<Bitmap>> mLoadedPreviews = new HashMap<>(); 134 private final ArrayList<SoftReference<Bitmap>> mUnusedBitmaps = new ArrayList<>(); 135 136 private final Context mContext; 137 private final int mAppIconSize; 138 private final IconCache mIconCache; 139 private final AppWidgetManagerCompat mManager; 140 141 private int mPreviewBitmapWidth; 142 private int mPreviewBitmapHeight; 143 private String mSize; 144 private PagedViewCellLayout mWidgetSpacingLayout; 145 146 private String mCachedSelectQuery; 147 148 149 private CacheDb mDb; 150 151 private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); 152 153 public WidgetPreviewLoader(Context context) { 154 LauncherAppState app = LauncherAppState.getInstance(); 155 DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); 156 157 mContext = context; 158 mAppIconSize = grid.iconSizePx; 159 mIconCache = app.getIconCache(); 160 mManager = AppWidgetManagerCompat.getInstance(context); 161 162 mDb = app.getWidgetPreviewCacheDb(); 163 164 SharedPreferences sp = context.getSharedPreferences( 165 LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); 166 final String lastVersionName = sp.getString(ANDROID_INCREMENTAL_VERSION_NAME_KEY, null); 167 final String versionName = android.os.Build.VERSION.INCREMENTAL; 168 if (!versionName.equals(lastVersionName)) { 169 // clear all the previews whenever the system version changes, to ensure that previews 170 // are up-to-date for any apps that might have been updated with the system 171 clearDb(); 172 173 SharedPreferences.Editor editor = sp.edit(); 174 editor.putString(ANDROID_INCREMENTAL_VERSION_NAME_KEY, versionName); 175 editor.commit(); 176 } 177 } 178 179 public void recreateDb() { 180 LauncherAppState app = LauncherAppState.getInstance(); 181 app.recreateWidgetPreviewDb(); 182 mDb = app.getWidgetPreviewCacheDb(); 183 } 184 185 public void setPreviewSize(int previewWidth, int previewHeight, 186 PagedViewCellLayout widgetSpacingLayout) { 187 mPreviewBitmapWidth = previewWidth; 188 mPreviewBitmapHeight = previewHeight; 189 mSize = previewWidth + "x" + previewHeight; 190 mWidgetSpacingLayout = widgetSpacingLayout; 191 } 192 193 public Bitmap getPreview(final Object o) { 194 final String name = getObjectName(o); 195 final String packageName = getObjectPackage(o); 196 // check if the package is valid 197 synchronized(sInvalidPackages) { 198 boolean packageValid = !sInvalidPackages.contains(packageName); 199 if (!packageValid) { 200 return null; 201 } 202 } 203 synchronized(mLoadedPreviews) { 204 // check if it exists in our existing cache 205 if (mLoadedPreviews.containsKey(name)) { 206 WeakReference<Bitmap> bitmapReference = mLoadedPreviews.get(name); 207 Bitmap bitmap = bitmapReference.get(); 208 if (bitmap != null) { 209 return bitmap; 210 } 211 } 212 } 213 214 Bitmap unusedBitmap = null; 215 synchronized(mUnusedBitmaps) { 216 // not in cache; we need to load it from the db 217 while (unusedBitmap == null && mUnusedBitmaps.size() > 0) { 218 Bitmap candidate = mUnusedBitmaps.remove(0).get(); 219 if (candidate != null && candidate.isMutable() && 220 candidate.getWidth() == mPreviewBitmapWidth && 221 candidate.getHeight() == mPreviewBitmapHeight) { 222 unusedBitmap = candidate; 223 } 224 } 225 if (unusedBitmap != null) { 226 final Canvas c = mCachedAppWidgetPreviewCanvas.get(); 227 c.setBitmap(unusedBitmap); 228 c.drawColor(0, PorterDuff.Mode.CLEAR); 229 c.setBitmap(null); 230 } 231 } 232 233 if (unusedBitmap == null) { 234 unusedBitmap = Bitmap.createBitmap(mPreviewBitmapWidth, mPreviewBitmapHeight, 235 Bitmap.Config.ARGB_8888); 236 } 237 Bitmap preview = readFromDb(name, unusedBitmap); 238 239 if (preview != null) { 240 synchronized(mLoadedPreviews) { 241 mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview)); 242 } 243 return preview; 244 } else { 245 // it's not in the db... we need to generate it 246 final Bitmap generatedPreview = generatePreview(o, unusedBitmap); 247 preview = generatedPreview; 248 if (preview != unusedBitmap) { 249 throw new RuntimeException("generatePreview is not recycling the bitmap " + o); 250 } 251 252 synchronized(mLoadedPreviews) { 253 mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview)); 254 } 255 256 // write to db on a thread pool... this can be done lazily and improves the performance 257 // of the first time widget previews are loaded 258 new AsyncTask<Void, Void, Void>() { 259 public Void doInBackground(Void ... args) { 260 writeToDb(o, generatedPreview); 261 return null; 262 } 263 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); 264 265 return preview; 266 } 267 } 268 269 public void recycleBitmap(Object o, Bitmap bitmapToRecycle) { 270 String name = getObjectName(o); 271 synchronized (mLoadedPreviews) { 272 if (mLoadedPreviews.containsKey(name)) { 273 Bitmap b = mLoadedPreviews.get(name).get(); 274 if (b == bitmapToRecycle) { 275 mLoadedPreviews.remove(name); 276 if (bitmapToRecycle.isMutable()) { 277 synchronized (mUnusedBitmaps) { 278 mUnusedBitmaps.add(new SoftReference<Bitmap>(b)); 279 } 280 } 281 } else { 282 throw new RuntimeException("Bitmap passed in doesn't match up"); 283 } 284 } 285 } 286 } 287 288 static class CacheDb extends SQLiteOpenHelper { 289 final static int DB_VERSION = 2; 290 final static String DB_NAME = "widgetpreviews.db"; 291 final static String TABLE_NAME = "shortcut_and_widget_previews"; 292 final static String COLUMN_NAME = "name"; 293 final static String COLUMN_SIZE = "size"; 294 final static String COLUMN_PREVIEW_BITMAP = "preview_bitmap"; 295 Context mContext; 296 297 public CacheDb(Context context) { 298 super(context, new File(context.getCacheDir(), DB_NAME).getPath(), null, DB_VERSION); 299 // Store the context for later use 300 mContext = context; 301 } 302 303 @Override 304 public void onCreate(SQLiteDatabase database) { 305 database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + 306 COLUMN_NAME + " TEXT NOT NULL, " + 307 COLUMN_SIZE + " TEXT NOT NULL, " + 308 COLUMN_PREVIEW_BITMAP + " BLOB NOT NULL, " + 309 "PRIMARY KEY (" + COLUMN_NAME + ", " + COLUMN_SIZE + ") " + 310 ");"); 311 } 312 313 @Override 314 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 315 if (oldVersion != newVersion) { 316 // Delete all the records; they'll be repopulated as this is a cache 317 db.execSQL("DELETE FROM " + TABLE_NAME); 318 } 319 } 320 } 321 322 private static final String WIDGET_PREFIX = "Widget:"; 323 private static final String SHORTCUT_PREFIX = "Shortcut:"; 324 325 private static String getObjectName(Object o) { 326 // should cache the string builder 327 StringBuilder sb = new StringBuilder(); 328 String output; 329 if (o instanceof AppWidgetProviderInfo) { 330 sb.append(WIDGET_PREFIX); 331 sb.append(((AppWidgetProviderInfo) o).toString()); 332 output = sb.toString(); 333 sb.setLength(0); 334 } else { 335 sb.append(SHORTCUT_PREFIX); 336 337 ResolveInfo info = (ResolveInfo) o; 338 sb.append(new ComponentName(info.activityInfo.packageName, 339 info.activityInfo.name).flattenToString()); 340 output = sb.toString(); 341 sb.setLength(0); 342 } 343 return output; 344 } 345 346 private String getObjectPackage(Object o) { 347 if (o instanceof AppWidgetProviderInfo) { 348 return ((AppWidgetProviderInfo) o).provider.getPackageName(); 349 } else { 350 ResolveInfo info = (ResolveInfo) o; 351 return info.activityInfo.packageName; 352 } 353 } 354 355 private void writeToDb(Object o, Bitmap preview) { 356 String name = getObjectName(o); 357 SQLiteDatabase db = mDb.getWritableDatabase(); 358 ContentValues values = new ContentValues(); 359 360 values.put(CacheDb.COLUMN_NAME, name); 361 ByteArrayOutputStream stream = new ByteArrayOutputStream(); 362 preview.compress(Bitmap.CompressFormat.PNG, 100, stream); 363 values.put(CacheDb.COLUMN_PREVIEW_BITMAP, stream.toByteArray()); 364 values.put(CacheDb.COLUMN_SIZE, mSize); 365 try { 366 db.insert(CacheDb.TABLE_NAME, null, values); 367 } catch (SQLiteDiskIOException e) { 368 recreateDb(); 369 } catch (SQLiteCantOpenDatabaseException e) { 370 dumpOpenFiles(); 371 throw e; 372 } 373 } 374 375 private void clearDb() { 376 SQLiteDatabase db = mDb.getWritableDatabase(); 377 // Delete everything 378 try { 379 db.delete(CacheDb.TABLE_NAME, null, null); 380 } catch (SQLiteDiskIOException e) { 381 } catch (SQLiteCantOpenDatabaseException e) { 382 dumpOpenFiles(); 383 throw e; 384 } 385 } 386 387 public static void removePackageFromDb(final CacheDb cacheDb, final String packageName) { 388 synchronized(sInvalidPackages) { 389 sInvalidPackages.add(packageName); 390 } 391 new AsyncTask<Void, Void, Void>() { 392 public Void doInBackground(Void ... args) { 393 SQLiteDatabase db = cacheDb.getWritableDatabase(); 394 try { 395 db.delete(CacheDb.TABLE_NAME, 396 CacheDb.COLUMN_NAME + " LIKE ? OR " + 397 CacheDb.COLUMN_NAME + " LIKE ?", // SELECT query 398 new String[] { 399 WIDGET_PREFIX + packageName + "/%", 400 SHORTCUT_PREFIX + packageName + "/%" 401 } // args to SELECT query 402 ); 403 } catch (SQLiteDiskIOException e) { 404 } catch (SQLiteCantOpenDatabaseException e) { 405 dumpOpenFiles(); 406 throw e; 407 } 408 synchronized(sInvalidPackages) { 409 sInvalidPackages.remove(packageName); 410 } 411 return null; 412 } 413 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); 414 } 415 416 private static void removeItemFromDb(final CacheDb cacheDb, final String objectName) { 417 new AsyncTask<Void, Void, Void>() { 418 public Void doInBackground(Void ... args) { 419 SQLiteDatabase db = cacheDb.getWritableDatabase(); 420 try { 421 db.delete(CacheDb.TABLE_NAME, 422 CacheDb.COLUMN_NAME + " = ? ", // SELECT query 423 new String[] { objectName }); // args to SELECT query 424 } catch (SQLiteDiskIOException e) { 425 } catch (SQLiteCantOpenDatabaseException e) { 426 dumpOpenFiles(); 427 throw e; 428 } 429 return null; 430 } 431 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); 432 } 433 434 private Bitmap readFromDb(String name, Bitmap b) { 435 if (mCachedSelectQuery == null) { 436 mCachedSelectQuery = CacheDb.COLUMN_NAME + " = ? AND " + 437 CacheDb.COLUMN_SIZE + " = ?"; 438 } 439 SQLiteDatabase db = mDb.getReadableDatabase(); 440 Cursor result; 441 try { 442 result = db.query(CacheDb.TABLE_NAME, 443 new String[] { CacheDb.COLUMN_PREVIEW_BITMAP }, // cols to return 444 mCachedSelectQuery, // select query 445 new String[] { name, mSize }, // args to select query 446 null, 447 null, 448 null, 449 null); 450 } catch (SQLiteDiskIOException e) { 451 recreateDb(); 452 return null; 453 } catch (SQLiteCantOpenDatabaseException e) { 454 dumpOpenFiles(); 455 throw e; 456 } 457 if (result.getCount() > 0) { 458 result.moveToFirst(); 459 byte[] blob = result.getBlob(0); 460 result.close(); 461 final BitmapFactory.Options opts = mCachedBitmapFactoryOptions.get(); 462 opts.inBitmap = b; 463 opts.inSampleSize = 1; 464 try { 465 return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts); 466 } catch (IllegalArgumentException e) { 467 removeItemFromDb(mDb, name); 468 return null; 469 } 470 } else { 471 result.close(); 472 return null; 473 } 474 } 475 476 private Bitmap generatePreview(Object info, Bitmap preview) { 477 if (preview != null && 478 (preview.getWidth() != mPreviewBitmapWidth || 479 preview.getHeight() != mPreviewBitmapHeight)) { 480 throw new RuntimeException("Improperly sized bitmap passed as argument"); 481 } 482 if (info instanceof AppWidgetProviderInfo) { 483 return generateWidgetPreview((AppWidgetProviderInfo) info, preview); 484 } else { 485 return generateShortcutPreview( 486 (ResolveInfo) info, mPreviewBitmapWidth, mPreviewBitmapHeight, preview); 487 } 488 } 489 490 public Bitmap generateWidgetPreview(AppWidgetProviderInfo info, Bitmap preview) { 491 int[] cellSpans = Launcher.getSpanForWidget(mContext, info); 492 int maxWidth = maxWidthForWidgetPreview(cellSpans[0]); 493 int maxHeight = maxHeightForWidgetPreview(cellSpans[1]); 494 return generateWidgetPreview(info, cellSpans[0], cellSpans[1], 495 maxWidth, maxHeight, preview, null); 496 } 497 498 public int maxWidthForWidgetPreview(int spanX) { 499 return Math.min(mPreviewBitmapWidth, 500 mWidgetSpacingLayout.estimateCellWidth(spanX)); 501 } 502 503 public int maxHeightForWidgetPreview(int spanY) { 504 return Math.min(mPreviewBitmapHeight, 505 mWidgetSpacingLayout.estimateCellHeight(spanY)); 506 } 507 508 public Bitmap generateWidgetPreview(AppWidgetProviderInfo info, int cellHSpan, int cellVSpan, 509 int maxPreviewWidth, int maxPreviewHeight, Bitmap preview, int[] preScaledWidthOut) { 510 // Load the preview image if possible 511 if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE; 512 if (maxPreviewHeight < 0) maxPreviewHeight = Integer.MAX_VALUE; 513 514 Drawable drawable = null; 515 if (info.previewImage != 0) { 516 drawable = mManager.loadPreview(info); 517 if (drawable != null) { 518 drawable = mutateOnMainThread(drawable); 519 } else { 520 Log.w(TAG, "Can't load widget preview drawable 0x" + 521 Integer.toHexString(info.previewImage) + " for provider: " + info.provider); 522 } 523 } 524 525 int previewWidth; 526 int previewHeight; 527 Bitmap defaultPreview = null; 528 boolean widgetPreviewExists = (drawable != null); 529 if (widgetPreviewExists) { 530 previewWidth = drawable.getIntrinsicWidth(); 531 previewHeight = drawable.getIntrinsicHeight(); 532 } else { 533 // Generate a preview image if we couldn't load one 534 if (cellHSpan < 1) cellHSpan = 1; 535 if (cellVSpan < 1) cellVSpan = 1; 536 537 // This Drawable is not directly drawn, so there's no need to mutate it. 538 BitmapDrawable previewDrawable = (BitmapDrawable) mContext.getResources() 539 .getDrawable(R.drawable.widget_tile); 540 final int previewDrawableWidth = previewDrawable 541 .getIntrinsicWidth(); 542 final int previewDrawableHeight = previewDrawable 543 .getIntrinsicHeight(); 544 previewWidth = previewDrawableWidth * cellHSpan; 545 previewHeight = previewDrawableHeight * cellVSpan; 546 547 defaultPreview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); 548 final Canvas c = mCachedAppWidgetPreviewCanvas.get(); 549 c.setBitmap(defaultPreview); 550 Paint p = mDefaultAppWidgetPreviewPaint.get(); 551 if (p == null) { 552 p = new Paint(); 553 p.setShader(new BitmapShader(previewDrawable.getBitmap(), 554 Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)); 555 mDefaultAppWidgetPreviewPaint.set(p); 556 } 557 final Rect dest = mCachedAppWidgetPreviewDestRect.get(); 558 dest.set(0, 0, previewWidth, previewHeight); 559 c.drawRect(dest, p); 560 c.setBitmap(null); 561 562 // Draw the icon in the top left corner 563 int minOffset = (int) (mAppIconSize * WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE); 564 int smallestSide = Math.min(previewWidth, previewHeight); 565 float iconScale = Math.min((float) smallestSide 566 / (mAppIconSize + 2 * minOffset), 1f); 567 568 try { 569 Drawable icon = mManager.loadIcon(info, mIconCache); 570 if (icon != null) { 571 int hoffset = (int) ((previewDrawableWidth - mAppIconSize * iconScale) / 2); 572 int yoffset = (int) ((previewDrawableHeight - mAppIconSize * iconScale) / 2); 573 icon = mutateOnMainThread(icon); 574 renderDrawableToBitmap(icon, defaultPreview, hoffset, 575 yoffset, (int) (mAppIconSize * iconScale), 576 (int) (mAppIconSize * iconScale)); 577 } 578 } catch (Resources.NotFoundException e) { 579 } 580 } 581 582 // Scale to fit width only - let the widget preview be clipped in the 583 // vertical dimension 584 float scale = 1f; 585 if (preScaledWidthOut != null) { 586 preScaledWidthOut[0] = previewWidth; 587 } 588 if (previewWidth > maxPreviewWidth) { 589 scale = maxPreviewWidth / (float) previewWidth; 590 } 591 if (scale != 1f) { 592 previewWidth = (int) (scale * previewWidth); 593 previewHeight = (int) (scale * previewHeight); 594 } 595 596 // If a bitmap is passed in, we use it; otherwise, we create a bitmap of the right size 597 if (preview == null) { 598 preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); 599 } 600 601 // Draw the scaled preview into the final bitmap 602 int x = (preview.getWidth() - previewWidth) / 2; 603 if (widgetPreviewExists) { 604 renderDrawableToBitmap(drawable, preview, x, 0, previewWidth, 605 previewHeight); 606 } else { 607 final Canvas c = mCachedAppWidgetPreviewCanvas.get(); 608 final Rect src = mCachedAppWidgetPreviewSrcRect.get(); 609 final Rect dest = mCachedAppWidgetPreviewDestRect.get(); 610 c.setBitmap(preview); 611 src.set(0, 0, defaultPreview.getWidth(), defaultPreview.getHeight()); 612 dest.set(x, 0, x + previewWidth, previewHeight); 613 614 Paint p = mCachedAppWidgetPreviewPaint.get(); 615 if (p == null) { 616 p = new Paint(); 617 p.setFilterBitmap(true); 618 mCachedAppWidgetPreviewPaint.set(p); 619 } 620 c.drawBitmap(defaultPreview, src, dest, p); 621 c.setBitmap(null); 622 } 623 return mManager.getBadgeBitmap(info, preview); 624 } 625 626 private Bitmap generateShortcutPreview( 627 ResolveInfo info, int maxWidth, int maxHeight, Bitmap preview) { 628 Bitmap tempBitmap = mCachedShortcutPreviewBitmap.get(); 629 final Canvas c = mCachedShortcutPreviewCanvas.get(); 630 if (tempBitmap == null || 631 tempBitmap.getWidth() != maxWidth || 632 tempBitmap.getHeight() != maxHeight) { 633 tempBitmap = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); 634 mCachedShortcutPreviewBitmap.set(tempBitmap); 635 } else { 636 c.setBitmap(tempBitmap); 637 c.drawColor(0, PorterDuff.Mode.CLEAR); 638 c.setBitmap(null); 639 } 640 // Render the icon 641 Drawable icon = mutateOnMainThread(mIconCache.getFullResIcon(info)); 642 643 int paddingTop = mContext. 644 getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top); 645 int paddingLeft = mContext. 646 getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left); 647 int paddingRight = mContext. 648 getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right); 649 650 int scaledIconWidth = (maxWidth - paddingLeft - paddingRight); 651 652 renderDrawableToBitmap( 653 icon, tempBitmap, paddingLeft, paddingTop, scaledIconWidth, scaledIconWidth); 654 655 if (preview != null && 656 (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight)) { 657 throw new RuntimeException("Improperly sized bitmap passed as argument"); 658 } else if (preview == null) { 659 preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); 660 } 661 662 c.setBitmap(preview); 663 // Draw a desaturated/scaled version of the icon in the background as a watermark 664 Paint p = mCachedShortcutPreviewPaint.get(); 665 if (p == null) { 666 p = new Paint(); 667 ColorMatrix colorMatrix = new ColorMatrix(); 668 colorMatrix.setSaturation(0); 669 p.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); 670 p.setAlpha((int) (255 * 0.06f)); 671 mCachedShortcutPreviewPaint.set(p); 672 } 673 c.drawBitmap(tempBitmap, 0, 0, p); 674 c.setBitmap(null); 675 676 renderDrawableToBitmap(icon, preview, 0, 0, mAppIconSize, mAppIconSize); 677 678 return preview; 679 } 680 681 private static void renderDrawableToBitmap( 682 Drawable d, Bitmap bitmap, int x, int y, int w, int h) { 683 if (bitmap != null) { 684 Canvas c = new Canvas(bitmap); 685 Rect oldBounds = d.copyBounds(); 686 d.setBounds(x, y, x + w, y + h); 687 d.draw(c); 688 d.setBounds(oldBounds); // Restore the bounds 689 c.setBitmap(null); 690 } 691 } 692 693 private Drawable mutateOnMainThread(final Drawable drawable) { 694 try { 695 return mMainThreadExecutor.submit(new Callable<Drawable>() { 696 @Override 697 public Drawable call() throws Exception { 698 return drawable.mutate(); 699 } 700 }).get(); 701 } catch (InterruptedException e) { 702 Thread.currentThread().interrupt(); 703 throw new RuntimeException(e); 704 } catch (ExecutionException e) { 705 throw new RuntimeException(e); 706 } 707 } 708 709 private static final int MAX_OPEN_FILES = 1024; 710 private static final int SAMPLE_RATE = 23; 711 /** 712 * Dumps all files that are open in this process without allocating a file descriptor. 713 */ 714 private static void dumpOpenFiles() { 715 try { 716 Log.i(TAG, "DUMP OF OPEN FILES (sample rate: 1 every " + SAMPLE_RATE + "):"); 717 final String TYPE_APK = "apk"; 718 final String TYPE_JAR = "jar"; 719 final String TYPE_PIPE = "pipe"; 720 final String TYPE_SOCKET = "socket"; 721 final String TYPE_DB = "db"; 722 final String TYPE_ANON_INODE = "anon_inode"; 723 final String TYPE_DEV = "dev"; 724 final String TYPE_NON_FS = "non-fs"; 725 final String TYPE_OTHER = "other"; 726 List<String> types = Arrays.asList(TYPE_APK, TYPE_JAR, TYPE_PIPE, TYPE_SOCKET, TYPE_DB, 727 TYPE_ANON_INODE, TYPE_DEV, TYPE_NON_FS, TYPE_OTHER); 728 int[] count = new int[types.size()]; 729 int[] duplicates = new int[types.size()]; 730 HashSet<String> files = new HashSet<String>(); 731 int total = 0; 732 for (int i = 0; i < MAX_OPEN_FILES; i++) { 733 // This is a gigantic hack but unfortunately the only way to resolve an fd 734 // to a file name. Note that we have to loop over all possible fds because 735 // reading the directory would require allocating a new fd. The kernel is 736 // currently implemented such that no fd is larger then the current rlimit, 737 // which is why it's safe to loop over them in such a way. 738 String fd = "/proc/self/fd/" + i; 739 try { 740 // getCanonicalPath() uses readlink behind the scene which doesn't require 741 // a file descriptor. 742 String resolved = new File(fd).getCanonicalPath(); 743 int type = types.indexOf(TYPE_OTHER); 744 if (resolved.startsWith("/dev/")) { 745 type = types.indexOf(TYPE_DEV); 746 } else if (resolved.endsWith(".apk")) { 747 type = types.indexOf(TYPE_APK); 748 } else if (resolved.endsWith(".jar")) { 749 type = types.indexOf(TYPE_JAR); 750 } else if (resolved.contains("/fd/pipe:")) { 751 type = types.indexOf(TYPE_PIPE); 752 } else if (resolved.contains("/fd/socket:")) { 753 type = types.indexOf(TYPE_SOCKET); 754 } else if (resolved.contains("/fd/anon_inode:")) { 755 type = types.indexOf(TYPE_ANON_INODE); 756 } else if (resolved.endsWith(".db") || resolved.contains("/databases/")) { 757 type = types.indexOf(TYPE_DB); 758 } else if (resolved.startsWith("/proc/") && resolved.contains("/fd/")) { 759 // Those are the files that don't point anywhere on the file system. 760 // getCanonicalPath() wrongly interprets these as relative symlinks and 761 // resolves them within /proc/<pid>/fd/. 762 type = types.indexOf(TYPE_NON_FS); 763 } 764 count[type]++; 765 total++; 766 if (files.contains(resolved)) { 767 duplicates[type]++; 768 } 769 files.add(resolved); 770 if (total % SAMPLE_RATE == 0) { 771 Log.i(TAG, " fd " + i + ": " + resolved 772 + " (" + types.get(type) + ")"); 773 } 774 } catch (IOException e) { 775 // Ignoring exceptions for non-existing file descriptors. 776 } 777 } 778 for (int i = 0; i < types.size(); i++) { 779 Log.i(TAG, String.format("Open %10s files: %4d total, %4d duplicates", 780 types.get(i), count[i], duplicates[i])); 781 } 782 } catch (Throwable t) { 783 // Catch everything. This is called from an exception handler that we shouldn't upset. 784 Log.e(TAG, "Unable to log open files.", t); 785 } 786 } 787 } 788