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