1package com.android.launcher3;
2
3import android.content.ComponentName;
4import android.content.ContentValues;
5import android.content.Context;
6import android.content.pm.PackageInfo;
7import android.content.pm.PackageManager;
8import android.content.pm.PackageManager.NameNotFoundException;
9import android.content.res.Resources;
10import android.database.Cursor;
11import android.database.SQLException;
12import android.database.sqlite.SQLiteDatabase;
13import android.graphics.Bitmap;
14import android.graphics.Bitmap.Config;
15import android.graphics.BitmapFactory;
16import android.graphics.Canvas;
17import android.graphics.Color;
18import android.graphics.Paint;
19import android.graphics.PorterDuff;
20import android.graphics.PorterDuffXfermode;
21import android.graphics.Rect;
22import android.graphics.RectF;
23import android.graphics.drawable.Drawable;
24import android.os.AsyncTask;
25import android.os.Build;
26import android.os.CancellationSignal;
27import android.os.Handler;
28import android.os.UserHandle;
29import android.support.annotation.Nullable;
30import android.support.v4.graphics.ColorUtils;
31import android.util.Log;
32import android.util.LongSparseArray;
33
34import com.android.launcher3.compat.AppWidgetManagerCompat;
35import com.android.launcher3.compat.ShortcutConfigActivityInfo;
36import com.android.launcher3.compat.UserManagerCompat;
37import com.android.launcher3.graphics.LauncherIcons;
38import com.android.launcher3.graphics.ShadowGenerator;
39import com.android.launcher3.model.WidgetItem;
40import com.android.launcher3.util.ComponentKey;
41import com.android.launcher3.util.PackageUserKey;
42import com.android.launcher3.util.Preconditions;
43import com.android.launcher3.util.SQLiteCacheHelper;
44import com.android.launcher3.util.Thunk;
45import com.android.launcher3.widget.WidgetCell;
46
47import java.util.ArrayList;
48import java.util.Collections;
49import java.util.HashMap;
50import java.util.HashSet;
51import java.util.Set;
52import java.util.WeakHashMap;
53import java.util.concurrent.Callable;
54import java.util.concurrent.ExecutionException;
55
56public 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
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     */
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
126        public CacheDb(Context context) {
127            super(context, LauncherFiles.WIDGET_PREVIEWS_DB, DB_VERSION, TABLE_NAME);
128        }
129
130        @Override
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
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
157    public void removePackage(String packageName, UserHandle user) {
158        removePackage(packageName, user, mUserManager.getSerialNumberForUser(user));
159    }
160
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     */
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     */
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
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     */
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
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
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
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