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