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