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