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