1 package com.android.launcher3; 2 3 import android.content.ComponentName; 4 import android.content.ContentValues; 5 import android.content.Context; 6 import android.content.pm.PackageInfo; 7 import android.content.pm.PackageManager.NameNotFoundException; 8 import android.content.pm.ResolveInfo; 9 import android.content.res.Resources; 10 import android.database.Cursor; 11 import android.database.SQLException; 12 import android.database.sqlite.SQLiteDatabase; 13 import android.graphics.Bitmap; 14 import android.graphics.Bitmap.Config; 15 import android.graphics.BitmapFactory; 16 import android.graphics.Canvas; 17 import android.graphics.ColorMatrix; 18 import android.graphics.ColorMatrixColorFilter; 19 import android.graphics.Paint; 20 import android.graphics.PorterDuff; 21 import android.graphics.Rect; 22 import android.graphics.RectF; 23 import android.graphics.drawable.BitmapDrawable; 24 import android.graphics.drawable.Drawable; 25 import android.os.AsyncTask; 26 import android.os.Handler; 27 import android.util.Log; 28 import android.util.LongSparseArray; 29 30 import com.android.launcher3.compat.AppWidgetManagerCompat; 31 import com.android.launcher3.compat.UserHandleCompat; 32 import com.android.launcher3.compat.UserManagerCompat; 33 import com.android.launcher3.util.ComponentKey; 34 import com.android.launcher3.util.SQLiteCacheHelper; 35 import com.android.launcher3.util.Thunk; 36 import com.android.launcher3.widget.WidgetCell; 37 38 import java.util.ArrayList; 39 import java.util.Collections; 40 import java.util.HashMap; 41 import java.util.HashSet; 42 import java.util.Set; 43 import java.util.WeakHashMap; 44 import java.util.concurrent.Callable; 45 import java.util.concurrent.ExecutionException; 46 47 public class WidgetPreviewLoader { 48 49 private static final String TAG = "WidgetPreviewLoader"; 50 private static final boolean DEBUG = false; 51 52 private static final float WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE = 0.25f; 53 54 private final HashMap<String, long[]> mPackageVersions = new HashMap<>(); 55 56 /** 57 * Weak reference objects, do not prevent their referents from being made finalizable, 58 * finalized, and then reclaimed. 59 * Note: synchronized block used for this variable is expensive and the block should always 60 * be posted to a background thread. 61 */ 62 @Thunk final Set<Bitmap> mUnusedBitmaps = 63 Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>()); 64 65 private final Context mContext; 66 private final IconCache mIconCache; 67 private final UserManagerCompat mUserManager; 68 private final AppWidgetManagerCompat mWidgetManager; 69 private final CacheDb mDb; 70 private final int mProfileBadgeMargin; 71 72 private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); 73 @Thunk final Handler mWorkerHandler; 74 WidgetPreviewLoader(Context context, IconCache iconCache)75 public WidgetPreviewLoader(Context context, IconCache iconCache) { 76 mContext = context; 77 mIconCache = iconCache; 78 mWidgetManager = AppWidgetManagerCompat.getInstance(context); 79 mUserManager = UserManagerCompat.getInstance(context); 80 mDb = new CacheDb(context); 81 mWorkerHandler = new Handler(LauncherModel.getWorkerLooper()); 82 mProfileBadgeMargin = context.getResources() 83 .getDimensionPixelSize(R.dimen.profile_badge_margin); 84 } 85 86 /** 87 * Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be 88 * called on UI thread 89 * 90 * @param o either {@link LauncherAppWidgetProviderInfo} or {@link ResolveInfo} 91 * @return a request id which can be used to cancel the request. 92 */ getPreview(final Object o, int previewWidth, int previewHeight, WidgetCell caller)93 public PreviewLoadRequest getPreview(final Object o, int previewWidth, 94 int previewHeight, WidgetCell caller) { 95 String size = previewWidth + "x" + previewHeight; 96 WidgetCacheKey key = getObjectKey(o, size); 97 98 PreviewLoadTask task = new PreviewLoadTask(key, o, previewWidth, previewHeight, caller); 99 task.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR); 100 return new PreviewLoadRequest(task); 101 } 102 103 /** 104 * The DB holds the generated previews for various components. Previews can also have different 105 * sizes (landscape vs portrait). 106 */ 107 private static class CacheDb extends SQLiteCacheHelper { 108 private static final int DB_VERSION = 4; 109 110 private static final String TABLE_NAME = "shortcut_and_widget_previews"; 111 private static final String COLUMN_COMPONENT = "componentName"; 112 private static final String COLUMN_USER = "profileId"; 113 private static final String COLUMN_SIZE = "size"; 114 private static final String COLUMN_PACKAGE = "packageName"; 115 private static final String COLUMN_LAST_UPDATED = "lastUpdated"; 116 private static final String COLUMN_VERSION = "version"; 117 private static final String COLUMN_PREVIEW_BITMAP = "preview_bitmap"; 118 CacheDb(Context context)119 public CacheDb(Context context) { 120 super(context, LauncherFiles.WIDGET_PREVIEWS_DB, DB_VERSION, TABLE_NAME); 121 } 122 123 @Override onCreateTable(SQLiteDatabase database)124 public void onCreateTable(SQLiteDatabase database) { 125 database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + 126 COLUMN_COMPONENT + " TEXT NOT NULL, " + 127 COLUMN_USER + " INTEGER NOT NULL, " + 128 COLUMN_SIZE + " TEXT NOT NULL, " + 129 COLUMN_PACKAGE + " TEXT NOT NULL, " + 130 COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " + 131 COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " + 132 COLUMN_PREVIEW_BITMAP + " BLOB, " + 133 "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ", " + COLUMN_SIZE + ") " + 134 ");"); 135 } 136 } 137 getObjectKey(Object o, String size)138 private WidgetCacheKey getObjectKey(Object o, String size) { 139 // should cache the string builder 140 if (o instanceof LauncherAppWidgetProviderInfo) { 141 LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) o; 142 return new WidgetCacheKey(info.provider, mWidgetManager.getUser(info), size); 143 } else { 144 ResolveInfo info = (ResolveInfo) o; 145 return new WidgetCacheKey( 146 new ComponentName(info.activityInfo.packageName, info.activityInfo.name), 147 UserHandleCompat.myUserHandle(), size); 148 } 149 } 150 writeToDb(WidgetCacheKey key, long[] versions, Bitmap preview)151 @Thunk void writeToDb(WidgetCacheKey key, long[] versions, Bitmap preview) { 152 ContentValues values = new ContentValues(); 153 values.put(CacheDb.COLUMN_COMPONENT, key.componentName.flattenToShortString()); 154 values.put(CacheDb.COLUMN_USER, mUserManager.getSerialNumberForUser(key.user)); 155 values.put(CacheDb.COLUMN_SIZE, key.size); 156 values.put(CacheDb.COLUMN_PACKAGE, key.componentName.getPackageName()); 157 values.put(CacheDb.COLUMN_VERSION, versions[0]); 158 values.put(CacheDb.COLUMN_LAST_UPDATED, versions[1]); 159 values.put(CacheDb.COLUMN_PREVIEW_BITMAP, Utilities.flattenBitmap(preview)); 160 mDb.insertOrReplace(values); 161 } 162 removePackage(String packageName, UserHandleCompat user)163 public void removePackage(String packageName, UserHandleCompat user) { 164 removePackage(packageName, user, mUserManager.getSerialNumberForUser(user)); 165 } 166 removePackage(String packageName, UserHandleCompat user, long userSerial)167 private void removePackage(String packageName, UserHandleCompat user, long userSerial) { 168 synchronized(mPackageVersions) { 169 mPackageVersions.remove(packageName); 170 } 171 172 mDb.delete( 173 CacheDb.COLUMN_PACKAGE + " = ? AND " + CacheDb.COLUMN_USER + " = ?", 174 new String[]{packageName, Long.toString(userSerial)}); 175 } 176 177 /** 178 * Updates the persistent DB: 179 * 1. Any preview generated for an old package version is removed 180 * 2. Any preview for an absent package is removed 181 * This ensures that we remove entries for packages which changed while the launcher was dead. 182 */ removeObsoletePreviews(ArrayList<Object> list)183 public void removeObsoletePreviews(ArrayList<Object> list) { 184 Utilities.assertWorkerThread(); 185 186 LongSparseArray<HashSet<String>> validPackages = new LongSparseArray<>(); 187 188 for (Object obj : list) { 189 final UserHandleCompat user; 190 final String pkg; 191 if (obj instanceof ResolveInfo) { 192 user = UserHandleCompat.myUserHandle(); 193 pkg = ((ResolveInfo) obj).activityInfo.packageName; 194 } else { 195 LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) obj; 196 user = mWidgetManager.getUser(info); 197 pkg = info.provider.getPackageName(); 198 } 199 200 final long userId = mUserManager.getSerialNumberForUser(user); 201 HashSet<String> packages = validPackages.get(userId); 202 if (packages == null) { 203 packages = new HashSet<>(); 204 validPackages.put(userId, packages); 205 } 206 packages.add(pkg); 207 } 208 209 LongSparseArray<HashSet<String>> packagesToDelete = new LongSparseArray<>(); 210 Cursor c = null; 211 try { 212 c = mDb.query( 213 new String[]{CacheDb.COLUMN_USER, CacheDb.COLUMN_PACKAGE, 214 CacheDb.COLUMN_LAST_UPDATED, CacheDb.COLUMN_VERSION}, 215 null, null); 216 while (c.moveToNext()) { 217 long userId = c.getLong(0); 218 String pkg = c.getString(1); 219 long lastUpdated = c.getLong(2); 220 long version = c.getLong(3); 221 222 HashSet<String> packages = validPackages.get(userId); 223 if (packages != null && packages.contains(pkg)) { 224 long[] versions = getPackageVersion(pkg); 225 if (versions[0] == version && versions[1] == lastUpdated) { 226 // Every thing checks out 227 continue; 228 } 229 } 230 231 // We need to delete this package. 232 packages = packagesToDelete.get(userId); 233 if (packages == null) { 234 packages = new HashSet<>(); 235 packagesToDelete.put(userId, packages); 236 } 237 packages.add(pkg); 238 } 239 240 for (int i = 0; i < packagesToDelete.size(); i++) { 241 long userId = packagesToDelete.keyAt(i); 242 UserHandleCompat user = mUserManager.getUserForSerialNumber(userId); 243 for (String pkg : packagesToDelete.valueAt(i)) { 244 removePackage(pkg, user, userId); 245 } 246 } 247 } catch (SQLException e) { 248 Log.e(TAG, "Error updating widget previews", e); 249 } finally { 250 if (c != null) { 251 c.close(); 252 } 253 } 254 } 255 256 /** 257 * Reads the preview bitmap from the DB or null if the preview is not in the DB. 258 */ readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask)259 @Thunk Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask) { 260 Cursor cursor = null; 261 try { 262 cursor = mDb.query( 263 new String[]{CacheDb.COLUMN_PREVIEW_BITMAP}, 264 CacheDb.COLUMN_COMPONENT + " = ? AND " + CacheDb.COLUMN_USER + " = ? AND " 265 + CacheDb.COLUMN_SIZE + " = ?", 266 new String[]{ 267 key.componentName.flattenToString(), 268 Long.toString(mUserManager.getSerialNumberForUser(key.user)), 269 key.size 270 }); 271 // If cancelled, skip getting the blob and decoding it into a bitmap 272 if (loadTask.isCancelled()) { 273 return null; 274 } 275 if (cursor.moveToNext()) { 276 byte[] blob = cursor.getBlob(0); 277 BitmapFactory.Options opts = new BitmapFactory.Options(); 278 opts.inBitmap = recycle; 279 try { 280 if (!loadTask.isCancelled()) { 281 return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts); 282 } 283 } catch (Exception e) { 284 return null; 285 } 286 } 287 } catch (SQLException e) { 288 Log.w(TAG, "Error loading preview from DB", e); 289 } finally { 290 if (cursor != null) { 291 cursor.close(); 292 } 293 } 294 return null; 295 } 296 generatePreview(Launcher launcher, Object info, Bitmap recycle, int previewWidth, int previewHeight)297 @Thunk Bitmap generatePreview(Launcher launcher, Object info, Bitmap recycle, 298 int previewWidth, int previewHeight) { 299 if (info instanceof LauncherAppWidgetProviderInfo) { 300 return generateWidgetPreview(launcher, (LauncherAppWidgetProviderInfo) info, 301 previewWidth, recycle, null); 302 } else { 303 return generateShortcutPreview(launcher, 304 (ResolveInfo) info, previewWidth, previewHeight, recycle); 305 } 306 } 307 308 /** 309 * Generates the widget preview from either the {@link AppWidgetManagerCompat} or cache 310 * and add badge at the bottom right corner. 311 * 312 * @param launcher 313 * @param info information about the widget 314 * @param maxPreviewWidth width of the preview on either workspace or tray 315 * @param preview bitmap that can be recycled 316 * @param preScaledWidthOut return the width of the returned bitmap 317 * @return 318 */ generateWidgetPreview(Launcher launcher, LauncherAppWidgetProviderInfo info, int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut)319 public Bitmap generateWidgetPreview(Launcher launcher, LauncherAppWidgetProviderInfo info, 320 int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) { 321 // Load the preview image if possible 322 if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE; 323 324 Drawable drawable = null; 325 if (info.previewImage != 0) { 326 drawable = mWidgetManager.loadPreview(info); 327 if (drawable != null) { 328 drawable = mutateOnMainThread(drawable); 329 } else { 330 Log.w(TAG, "Can't load widget preview drawable 0x" + 331 Integer.toHexString(info.previewImage) + " for provider: " + info.provider); 332 } 333 } 334 335 final boolean widgetPreviewExists = (drawable != null); 336 final int spanX = info.spanX; 337 final int spanY = info.spanY; 338 339 int previewWidth; 340 int previewHeight; 341 342 Bitmap tileBitmap = null; 343 344 if (widgetPreviewExists) { 345 previewWidth = drawable.getIntrinsicWidth(); 346 previewHeight = drawable.getIntrinsicHeight(); 347 } else { 348 // Generate a preview image if we couldn't load one 349 tileBitmap = ((BitmapDrawable) mContext.getResources().getDrawable( 350 R.drawable.widget_tile)).getBitmap(); 351 previewWidth = tileBitmap.getWidth() * spanX; 352 previewHeight = tileBitmap.getHeight() * spanY; 353 } 354 355 // Scale to fit width only - let the widget preview be clipped in the 356 // vertical dimension 357 float scale = 1f; 358 if (preScaledWidthOut != null) { 359 preScaledWidthOut[0] = previewWidth; 360 } 361 if (previewWidth > maxPreviewWidth) { 362 scale = (maxPreviewWidth - 2 * mProfileBadgeMargin) / (float) (previewWidth); 363 } 364 if (scale != 1f) { 365 previewWidth = (int) (scale * previewWidth); 366 previewHeight = (int) (scale * previewHeight); 367 } 368 369 // If a bitmap is passed in, we use it; otherwise, we create a bitmap of the right size 370 final Canvas c = new Canvas(); 371 if (preview == null) { 372 preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); 373 c.setBitmap(preview); 374 } else { 375 // Reusing bitmap. Clear it. 376 c.setBitmap(preview); 377 c.drawColor(0, PorterDuff.Mode.CLEAR); 378 } 379 380 // Draw the scaled preview into the final bitmap 381 int x = (preview.getWidth() - previewWidth) / 2; 382 if (widgetPreviewExists) { 383 drawable.setBounds(x, 0, x + previewWidth, previewHeight); 384 drawable.draw(c); 385 } else { 386 final Paint p = new Paint(); 387 p.setFilterBitmap(true); 388 int appIconSize = launcher.getDeviceProfile().iconSizePx; 389 390 // draw the spanX x spanY tiles 391 final Rect src = new Rect(0, 0, tileBitmap.getWidth(), tileBitmap.getHeight()); 392 393 float tileW = scale * tileBitmap.getWidth(); 394 float tileH = scale * tileBitmap.getHeight(); 395 final RectF dst = new RectF(0, 0, tileW, tileH); 396 397 float tx = x; 398 for (int i = 0; i < spanX; i++, tx += tileW) { 399 float ty = 0; 400 for (int j = 0; j < spanY; j++, ty += tileH) { 401 dst.offsetTo(tx, ty); 402 c.drawBitmap(tileBitmap, src, dst, p); 403 } 404 } 405 406 // Draw the icon in the top left corner 407 // TODO: use top right for RTL 408 int minOffset = (int) (appIconSize * WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE); 409 int smallestSide = Math.min(previewWidth, previewHeight); 410 float iconScale = Math.min((float) smallestSide / (appIconSize + 2 * minOffset), scale); 411 412 try { 413 Drawable icon = mWidgetManager.loadIcon(info, mIconCache); 414 if (icon != null) { 415 icon = mutateOnMainThread(icon); 416 int hoffset = (int) ((tileW - appIconSize * iconScale) / 2) + x; 417 int yoffset = (int) ((tileH - appIconSize * iconScale) / 2); 418 icon.setBounds(hoffset, yoffset, 419 hoffset + (int) (appIconSize * iconScale), 420 yoffset + (int) (appIconSize * iconScale)); 421 icon.draw(c); 422 } 423 } catch (Resources.NotFoundException e) { 424 } 425 c.setBitmap(null); 426 } 427 int imageWidth = Math.min(preview.getWidth(), previewWidth + mProfileBadgeMargin); 428 int imageHeight = Math.min(preview.getHeight(), previewHeight + mProfileBadgeMargin); 429 return mWidgetManager.getBadgeBitmap(info, preview, imageWidth, imageHeight); 430 } 431 generateShortcutPreview( Launcher launcher, ResolveInfo info, int maxWidth, int maxHeight, Bitmap preview)432 private Bitmap generateShortcutPreview( 433 Launcher launcher, ResolveInfo info, int maxWidth, int maxHeight, Bitmap preview) { 434 final Canvas c = new Canvas(); 435 if (preview == null) { 436 preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); 437 c.setBitmap(preview); 438 } else if (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight) { 439 throw new RuntimeException("Improperly sized bitmap passed as argument"); 440 } else { 441 // Reusing bitmap. Clear it. 442 c.setBitmap(preview); 443 c.drawColor(0, PorterDuff.Mode.CLEAR); 444 } 445 446 Drawable icon = mutateOnMainThread(mIconCache.getFullResIcon(info.activityInfo)); 447 icon.setFilterBitmap(true); 448 449 // Draw a desaturated/scaled version of the icon in the background as a watermark 450 ColorMatrix colorMatrix = new ColorMatrix(); 451 colorMatrix.setSaturation(0); 452 icon.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); 453 icon.setAlpha((int) (255 * 0.06f)); 454 455 Resources res = mContext.getResources(); 456 int paddingTop = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top); 457 int paddingLeft = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left); 458 int paddingRight = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right); 459 int scaledIconWidth = (maxWidth - paddingLeft - paddingRight); 460 icon.setBounds(paddingLeft, paddingTop, 461 paddingLeft + scaledIconWidth, paddingTop + scaledIconWidth); 462 icon.draw(c); 463 464 // Draw the final icon at top left corner. 465 // TODO: use top right for RTL 466 int appIconSize = launcher.getDeviceProfile().iconSizePx; 467 468 icon.setAlpha(255); 469 icon.setColorFilter(null); 470 icon.setBounds(0, 0, appIconSize, appIconSize); 471 icon.draw(c); 472 473 c.setBitmap(null); 474 return preview; 475 } 476 mutateOnMainThread(final Drawable drawable)477 private Drawable mutateOnMainThread(final Drawable drawable) { 478 try { 479 return mMainThreadExecutor.submit(new Callable<Drawable>() { 480 @Override 481 public Drawable call() throws Exception { 482 return drawable.mutate(); 483 } 484 }).get(); 485 } catch (InterruptedException e) { 486 Thread.currentThread().interrupt(); 487 throw new RuntimeException(e); 488 } catch (ExecutionException e) { 489 throw new RuntimeException(e); 490 } 491 } 492 493 /** 494 * @return an array of containing versionCode and lastUpdatedTime for the package. 495 */ 496 @Thunk long[] getPackageVersion(String packageName) { 497 synchronized (mPackageVersions) { 498 long[] versions = mPackageVersions.get(packageName); 499 if (versions == null) { 500 versions = new long[2]; 501 try { 502 PackageInfo info = mContext.getPackageManager().getPackageInfo(packageName, 0); 503 versions[0] = info.versionCode; 504 versions[1] = info.lastUpdateTime; 505 } catch (NameNotFoundException e) { 506 Log.e(TAG, "PackageInfo not found", e); 507 } 508 mPackageVersions.put(packageName, versions); 509 } 510 return versions; 511 } 512 } 513 514 /** 515 * A request Id which can be used by the client to cancel any request. 516 */ 517 public class PreviewLoadRequest { 518 519 @Thunk final PreviewLoadTask mTask; 520 521 public PreviewLoadRequest(PreviewLoadTask task) { 522 mTask = task; 523 } 524 525 public void cleanup() { 526 if (mTask != null) { 527 mTask.cancel(true); 528 } 529 530 // This only handles the case where the PreviewLoadTask is cancelled after the task has 531 // successfully completed (including having written to disk when necessary). In the 532 // other cases where it is cancelled while the task is running, it will be cleaned up 533 // in the tasks's onCancelled() call, and if cancelled while the task is writing to 534 // disk, it will be cancelled in the task's onPostExecute() call. 535 if (mTask.mBitmapToRecycle != null) { 536 mWorkerHandler.post(new Runnable() { 537 @Override 538 public void run() { 539 synchronized (mUnusedBitmaps) { 540 mUnusedBitmaps.add(mTask.mBitmapToRecycle); 541 } 542 mTask.mBitmapToRecycle = null; 543 } 544 }); 545 } 546 } 547 } 548 549 public class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap> { 550 @Thunk final WidgetCacheKey mKey; 551 private final Object mInfo; 552 private final int mPreviewHeight; 553 private final int mPreviewWidth; 554 private final WidgetCell mCaller; 555 @Thunk long[] mVersions; 556 @Thunk Bitmap mBitmapToRecycle; 557 558 PreviewLoadTask(WidgetCacheKey key, Object info, int previewWidth, 559 int previewHeight, WidgetCell caller) { 560 mKey = key; 561 mInfo = info; 562 mPreviewHeight = previewHeight; 563 mPreviewWidth = previewWidth; 564 mCaller = caller; 565 if (DEBUG) { 566 Log.d(TAG, String.format("%s, %s, %d, %d", 567 mKey, mInfo, mPreviewHeight, mPreviewWidth)); 568 } 569 } 570 571 @Override 572 protected Bitmap doInBackground(Void... params) { 573 Bitmap unusedBitmap = null; 574 575 // If already cancelled before this gets to run in the background, then return early 576 if (isCancelled()) { 577 return null; 578 } 579 synchronized (mUnusedBitmaps) { 580 // Check if we can re-use a bitmap 581 for (Bitmap candidate : mUnusedBitmaps) { 582 if (candidate != null && candidate.isMutable() && 583 candidate.getWidth() == mPreviewWidth && 584 candidate.getHeight() == mPreviewHeight) { 585 unusedBitmap = candidate; 586 mUnusedBitmaps.remove(unusedBitmap); 587 break; 588 } 589 } 590 } 591 592 // creating a bitmap is expensive. Do not do this inside synchronized block. 593 if (unusedBitmap == null) { 594 unusedBitmap = Bitmap.createBitmap(mPreviewWidth, mPreviewHeight, Config.ARGB_8888); 595 } 596 // If cancelled now, don't bother reading the preview from the DB 597 if (isCancelled()) { 598 return unusedBitmap; 599 } 600 Bitmap preview = readFromDb(mKey, unusedBitmap, this); 601 // Only consider generating the preview if we have not cancelled the task already 602 if (!isCancelled() && preview == null) { 603 // Fetch the version info before we generate the preview, so that, in-case the 604 // app was updated while we are generating the preview, we use the old version info, 605 // which would gets re-written next time. 606 mVersions = getPackageVersion(mKey.componentName.getPackageName()); 607 608 Launcher launcher = (Launcher) mCaller.getContext(); 609 610 // it's not in the db... we need to generate it 611 preview = generatePreview(launcher, mInfo, unusedBitmap, mPreviewWidth, mPreviewHeight); 612 } 613 return preview; 614 } 615 616 @Override 617 protected void onPostExecute(final Bitmap preview) { 618 mCaller.applyPreview(preview); 619 620 // Write the generated preview to the DB in the worker thread 621 if (mVersions != null) { 622 mWorkerHandler.post(new Runnable() { 623 @Override 624 public void run() { 625 if (!isCancelled()) { 626 // If we are still using this preview, then write it to the DB and then 627 // let the normal clear mechanism recycle the bitmap 628 writeToDb(mKey, mVersions, preview); 629 mBitmapToRecycle = preview; 630 } else { 631 // If we've already cancelled, then skip writing the bitmap to the DB 632 // and manually add the bitmap back to the recycled set 633 synchronized (mUnusedBitmaps) { 634 mUnusedBitmaps.add(preview); 635 } 636 } 637 } 638 }); 639 } else { 640 // If we don't need to write to disk, then ensure the preview gets recycled by 641 // the normal clear mechanism 642 mBitmapToRecycle = preview; 643 } 644 } 645 646 @Override 647 protected void onCancelled(final Bitmap preview) { 648 // If we've cancelled while the task is running, then can return the bitmap to the 649 // recycled set immediately. Otherwise, it will be recycled after the preview is written 650 // to disk. 651 if (preview != null) { 652 mWorkerHandler.post(new Runnable() { 653 @Override 654 public void run() { 655 synchronized (mUnusedBitmaps) { 656 mUnusedBitmaps.add(preview); 657 } 658 } 659 }); 660 } 661 } 662 } 663 664 private static final class WidgetCacheKey extends ComponentKey { 665 666 // TODO: remove dependency on size 667 @Thunk final String size; 668 669 public WidgetCacheKey(ComponentName componentName, UserHandleCompat user, String size) { 670 super(componentName, user); 671 this.size = size; 672 } 673 674 @Override 675 public int hashCode() { 676 return super.hashCode() ^ size.hashCode(); 677 } 678 679 @Override 680 public boolean equals(Object o) { 681 return super.equals(o) && ((WidgetCacheKey) o).size.equals(size); 682 } 683 } 684 } 685