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