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