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