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