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