Files
Lawnchair/src/com/android/launcher3/WidgetPreviewLoader.java
T
Stevie Kideckel 43a6be4b82 Remove intrinsic padding from widget previews
Also, scale down the preview size if it's wider than the target cell.

The intrinsic padding prevents the corners of the preview from being
rounded correctly and can lead to clipping or no rounding, especially at
larger sizes.

Bug: 187141692
Test: verified locally
Change-Id: Ie57ad57ba7c102330324bd0439acc55ff4fd83ff
2021-05-25 16:26:32 +00:00

739 lines
30 KiB
Java

package com.android.launcher3;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.CancellationSignal;
import android.os.Process;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.Pair;
import androidx.annotation.Nullable;
import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.icons.ShadowGenerator;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.pm.ShortcutConfigActivityInfo;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.SQLiteCacheHelper;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.WidgetManagerHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
public class WidgetPreviewLoader {
private static final String TAG = "WidgetPreviewLoader";
private static final boolean DEBUG = false;
private final HashMap<String, long[]> mPackageVersions = new HashMap<>();
/**
* Weak reference objects, do not prevent their referents from being made finalizable,
* finalized, and then reclaimed.
* Note: synchronized block used for this variable is expensive and the block should always
* be posted to a background thread.
*/
@Thunk final Set<Bitmap> mUnusedBitmaps = Collections.newSetFromMap(new WeakHashMap<>());
private final Context mContext;
private final IconCache mIconCache;
private final UserCache mUserCache;
private final CacheDb mDb;
private final UserHandle mMyUser = Process.myUserHandle();
private final ArrayMap<UserHandle, Bitmap> mUserBadges = new ArrayMap<>();
public WidgetPreviewLoader(Context context, IconCache iconCache) {
mContext = context;
mIconCache = iconCache;
mUserCache = UserCache.INSTANCE.get(context);
mDb = new CacheDb(context);
}
/**
* Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be
* called on UI thread
*
* @return a request id which can be used to cancel the request.
*/
public CancellationSignal getPreview(WidgetItem item, int previewWidth,
int previewHeight, WidgetCell caller) {
String size = previewWidth + "x" + previewHeight;
WidgetCacheKey key = new WidgetCacheKey(item.componentName, item.user, size);
PreviewLoadTask task = new PreviewLoadTask(key, item, previewWidth, previewHeight, caller);
task.executeOnExecutor(Executors.THREAD_POOL_EXECUTOR);
CancellationSignal signal = new CancellationSignal();
signal.setOnCancelListener(task);
return signal;
}
public void refresh() {
mDb.clear();
}
/**
* The DB holds the generated previews for various components. Previews can also have different
* sizes (landscape vs portrait).
*/
private static class CacheDb extends SQLiteCacheHelper {
private static final int DB_VERSION = 9;
private static final String TABLE_NAME = "shortcut_and_widget_previews";
private static final String COLUMN_COMPONENT = "componentName";
private static final String COLUMN_USER = "profileId";
private static final String COLUMN_SIZE = "size";
private static final String COLUMN_PACKAGE = "packageName";
private static final String COLUMN_LAST_UPDATED = "lastUpdated";
private static final String COLUMN_VERSION = "version";
private static final String COLUMN_PREVIEW_BITMAP = "preview_bitmap";
public CacheDb(Context context) {
super(context, LauncherFiles.WIDGET_PREVIEWS_DB, DB_VERSION, TABLE_NAME);
}
@Override
public void onCreateTable(SQLiteDatabase database) {
database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
COLUMN_COMPONENT + " TEXT NOT NULL, " +
COLUMN_USER + " INTEGER NOT NULL, " +
COLUMN_SIZE + " TEXT NOT NULL, " +
COLUMN_PACKAGE + " TEXT NOT NULL, " +
COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " +
COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " +
COLUMN_PREVIEW_BITMAP + " BLOB, " +
"PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ", " + COLUMN_SIZE + ") " +
");");
}
}
@Thunk void writeToDb(WidgetCacheKey key, long[] versions, Bitmap preview) {
ContentValues values = new ContentValues();
values.put(CacheDb.COLUMN_COMPONENT, key.componentName.flattenToShortString());
values.put(CacheDb.COLUMN_USER, mUserCache.getSerialNumberForUser(key.user));
values.put(CacheDb.COLUMN_SIZE, key.size);
values.put(CacheDb.COLUMN_PACKAGE, key.componentName.getPackageName());
values.put(CacheDb.COLUMN_VERSION, versions[0]);
values.put(CacheDb.COLUMN_LAST_UPDATED, versions[1]);
values.put(CacheDb.COLUMN_PREVIEW_BITMAP, GraphicsUtils.flattenBitmap(preview));
mDb.insertOrReplace(values);
}
public void removePackage(String packageName, UserHandle user) {
removePackage(packageName, user, mUserCache.getSerialNumberForUser(user));
}
private void removePackage(String packageName, UserHandle user, long userSerial) {
synchronized(mPackageVersions) {
mPackageVersions.remove(packageName);
}
mDb.delete(
CacheDb.COLUMN_PACKAGE + " = ? AND " + CacheDb.COLUMN_USER + " = ?",
new String[]{packageName, Long.toString(userSerial)});
}
/**
* Updates the persistent DB:
* 1. Any preview generated for an old package version is removed
* 2. Any preview for an absent package is removed
* This ensures that we remove entries for packages which changed while the launcher was dead.
*
* @param packageUser if provided, specifies that list only contains previews for the
* given package/user, otherwise the list contains all previews
*/
public void removeObsoletePreviews(ArrayList<? extends ComponentKey> list,
@Nullable PackageUserKey packageUser) {
Preconditions.assertWorkerThread();
LongSparseArray<HashSet<String>> validPackages = new LongSparseArray<>();
for (ComponentKey key : list) {
final long userId = mUserCache.getSerialNumberForUser(key.user);
HashSet<String> packages = validPackages.get(userId);
if (packages == null) {
packages = new HashSet<>();
validPackages.put(userId, packages);
}
packages.add(key.componentName.getPackageName());
}
LongSparseArray<HashSet<String>> packagesToDelete = new LongSparseArray<>();
long passedUserId = packageUser == null ? 0
: mUserCache.getSerialNumberForUser(packageUser.mUser);
Cursor c = null;
try {
c = mDb.query(
new String[]{CacheDb.COLUMN_USER, CacheDb.COLUMN_PACKAGE,
CacheDb.COLUMN_LAST_UPDATED, CacheDb.COLUMN_VERSION},
null, null);
while (c.moveToNext()) {
long userId = c.getLong(0);
String pkg = c.getString(1);
long lastUpdated = c.getLong(2);
long version = c.getLong(3);
if (packageUser != null && (!pkg.equals(packageUser.mPackageName)
|| userId != passedUserId)) {
// This preview is associated with a different package/user, no need to remove.
continue;
}
HashSet<String> packages = validPackages.get(userId);
if (packages != null && packages.contains(pkg)) {
long[] versions = getPackageVersion(pkg);
if (versions[0] == version && versions[1] == lastUpdated) {
// Every thing checks out
continue;
}
}
// We need to delete this package.
packages = packagesToDelete.get(userId);
if (packages == null) {
packages = new HashSet<>();
packagesToDelete.put(userId, packages);
}
packages.add(pkg);
}
for (int i = 0; i < packagesToDelete.size(); i++) {
long userId = packagesToDelete.keyAt(i);
UserHandle user = mUserCache.getUserForSerialNumber(userId);
for (String pkg : packagesToDelete.valueAt(i)) {
removePackage(pkg, user, userId);
}
}
} catch (SQLException e) {
Log.e(TAG, "Error updating widget previews", e);
} finally {
if (c != null) {
c.close();
}
}
}
/**
* Reads the preview bitmap from the DB or null if the preview is not in the DB.
*/
@Thunk Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask) {
Cursor cursor = null;
try {
cursor = mDb.query(
new String[]{CacheDb.COLUMN_PREVIEW_BITMAP},
CacheDb.COLUMN_COMPONENT + " = ? AND " + CacheDb.COLUMN_USER + " = ? AND "
+ CacheDb.COLUMN_SIZE + " = ?",
new String[]{
key.componentName.flattenToShortString(),
Long.toString(mUserCache.getSerialNumberForUser(key.user)),
key.size
});
// If cancelled, skip getting the blob and decoding it into a bitmap
if (loadTask.isCancelled()) {
return null;
}
if (cursor.moveToNext()) {
byte[] blob = cursor.getBlob(0);
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inBitmap = recycle;
try {
if (!loadTask.isCancelled()) {
return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts);
}
} catch (Exception e) {
return null;
}
}
} catch (SQLException e) {
Log.w(TAG, "Error loading preview from DB", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return null;
}
/**
* Returns generatedPreview for a widget and if the preview should be saved in persistent
* storage.
* @param launcher
* @param item
* @param recycle
* @param previewWidth
* @param previewHeight
* @return Pair<Bitmap, Boolean>
*/
private Pair<Bitmap, Boolean> generatePreview(BaseActivity launcher, WidgetItem item,
Bitmap recycle,
int previewWidth, int previewHeight) {
if (item.widgetInfo != null) {
return generateWidgetPreview(launcher, item.widgetInfo,
previewWidth, recycle, null);
} else {
return new Pair<>(generateShortcutPreview(launcher, item.activityInfo,
previewWidth, previewHeight, recycle), false);
}
}
/**
* Generates the widget preview from either the {@link WidgetManagerHelper} or cache
* and add badge at the bottom right corner.
*
* @param launcher
* @param info information about the widget
* @param maxPreviewWidth width of the preview on either workspace or tray
* @param preview bitmap that can be recycled
* @param preScaledWidthOut return the width of the returned bitmap
* @return Pair<Bitmap (the preview) , Boolean (should be stored in db)>
*/
public Pair<Bitmap, Boolean> generateWidgetPreview(BaseActivity launcher,
LauncherAppWidgetProviderInfo info,
int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) {
// Load the preview image if possible
if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE;
Drawable drawable = null;
if (info.previewImage != 0) {
try {
drawable = info.loadPreviewImage(mContext, 0);
} catch (OutOfMemoryError e) {
Log.w(TAG, "Error loading widget preview for: " + info.provider, e);
// During OutOfMemoryError, the previous heap stack is not affected. Catching
// an OOM error here should be safe & not affect other parts of launcher.
drawable = null;
}
if (drawable != null) {
drawable = mutateOnMainThread(drawable);
} else {
Log.w(TAG, "Can't load widget preview drawable 0x" +
Integer.toHexString(info.previewImage) + " for provider: " + info.provider);
}
}
final boolean widgetPreviewExists = (drawable != null);
final int spanX = info.spanX;
final int spanY = info.spanY;
int previewWidth;
int previewHeight;
boolean savePreviewImage = widgetPreviewExists || info.previewImage == 0;
if (widgetPreviewExists && drawable.getIntrinsicWidth() > 0
&& drawable.getIntrinsicHeight() > 0) {
previewWidth = drawable.getIntrinsicWidth();
previewHeight = drawable.getIntrinsicHeight();
} else {
DeviceProfile dp = launcher.getDeviceProfile();
int tileSize = Math.min(dp.cellWidthPx, dp.cellHeightPx);
previewWidth = tileSize * spanX;
previewHeight = tileSize * spanY;
}
// Scale to fit width only - let the widget preview be clipped in the
// vertical dimension
float scale = 1f;
if (preScaledWidthOut != null) {
preScaledWidthOut[0] = previewWidth;
}
if (previewWidth > maxPreviewWidth) {
scale = maxPreviewWidth / (float) (previewWidth);
}
if (scale != 1f) {
previewWidth = Math.max((int)(scale * previewWidth), 1);
previewHeight = Math.max((int)(scale * previewHeight), 1);
}
final Canvas c = new Canvas();
if (preview == null) {
// If no bitmap was provided, then allocate a new one with the right size.
preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);
c.setBitmap(preview);
} else {
// If a bitmap was passed in, attempt to reconfigure the bitmap to the same dimensions
// as the preview.
try {
preview.reconfigure(previewWidth, previewHeight, preview.getConfig());
} catch (IllegalArgumentException e) {
// This occurs if the preview can't be reconfigured for any reason. In this case,
// allocate a new bitmap with the right size.
preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);
}
c.setBitmap(preview);
c.drawColor(0, PorterDuff.Mode.CLEAR);
}
// Draw the scaled preview into the final bitmap
if (widgetPreviewExists) {
drawable.setBounds(0, 0, previewWidth, previewHeight);
drawable.draw(c);
} else {
RectF boxRect;
// Draw horizontal and vertical lines to represent individual columns.
final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
if (Utilities.ATLEAST_S) {
boxRect = new RectF(/* left= */ 0, /* top= */ 0, /* right= */
previewWidth, /* bottom= */ previewHeight);
p.setStyle(Paint.Style.FILL);
p.setColor(Color.WHITE);
float roundedCorner = mContext.getResources().getDimension(
android.R.dimen.system_app_widget_background_radius);
c.drawRoundRect(boxRect, roundedCorner, roundedCorner, p);
} else {
boxRect = drawBoxWithShadow(c, previewWidth, previewHeight);
}
p.setStyle(Paint.Style.STROKE);
p.setStrokeWidth(mContext.getResources()
.getDimension(R.dimen.widget_preview_cell_divider_width));
p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
float t = boxRect.left;
float tileSize = boxRect.width() / spanX;
for (int i = 1; i < spanX; i++) {
t += tileSize;
c.drawLine(t, 0, t, previewHeight, p);
}
t = boxRect.top;
tileSize = boxRect.height() / spanY;
for (int i = 1; i < spanY; i++) {
t += tileSize;
c.drawLine(0, t, previewWidth, t, p);
}
// Draw icon in the center.
try {
Drawable icon =
mIconCache.getFullResIcon(info.provider.getPackageName(), info.icon);
if (icon != null) {
int appIconSize = launcher.getDeviceProfile().iconSizePx;
int iconSize = (int) Math.min(appIconSize * scale,
Math.min(boxRect.width(), boxRect.height()));
icon = mutateOnMainThread(icon);
int hoffset = (previewWidth - iconSize) / 2;
int yoffset = (previewHeight - iconSize) / 2;
icon.setBounds(hoffset, yoffset, hoffset + iconSize, yoffset + iconSize);
icon.draw(c);
}
} catch (Resources.NotFoundException e) {
savePreviewImage = false;
}
c.setBitmap(null);
}
return new Pair<>(preview, savePreviewImage);
}
private RectF drawBoxWithShadow(Canvas c, int width, int height) {
Resources res = mContext.getResources();
ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.WHITE);
builder.shadowBlur = res.getDimension(R.dimen.widget_preview_shadow_blur);
builder.radius = res.getDimension(R.dimen.widget_preview_corner_radius);
builder.keyShadowDistance = res.getDimension(R.dimen.widget_preview_key_shadow_distance);
builder.bounds.set(builder.shadowBlur, builder.shadowBlur,
width - builder.shadowBlur,
height - builder.shadowBlur - builder.keyShadowDistance);
builder.drawShadow(c);
return builder.bounds;
}
private Bitmap generateShortcutPreview(BaseActivity launcher, ShortcutConfigActivityInfo info,
int maxWidth, int maxHeight, Bitmap preview) {
int iconSize = launcher.getDeviceProfile().allAppsIconSizePx;
int padding = launcher.getResources()
.getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
int size = iconSize + 2 * padding;
if (maxHeight < size || maxWidth < size) {
throw new RuntimeException("Max size is too small for preview");
}
final Canvas c = new Canvas();
if (preview == null || preview.getWidth() < size || preview.getHeight() < size) {
preview = Bitmap.createBitmap(size, size, Config.ARGB_8888);
c.setBitmap(preview);
} else {
if (preview.getWidth() > size || preview.getHeight() > size) {
preview.reconfigure(size, size, preview.getConfig());
}
// Reusing bitmap. Clear it.
c.setBitmap(preview);
c.drawColor(0, PorterDuff.Mode.CLEAR);
}
drawBoxWithShadow(c, size, size);
LauncherIcons li = LauncherIcons.obtain(mContext);
Drawable icon = li.createBadgedIconBitmap(
mutateOnMainThread(info.getFullResIcon(mIconCache)),
Process.myUserHandle(), 0).newIcon(launcher);
li.recycle();
icon.setBounds(padding, padding, padding + iconSize, padding + iconSize);
icon.draw(c);
c.setBitmap(null);
return preview;
}
private Drawable mutateOnMainThread(final Drawable drawable) {
try {
return MAIN_EXECUTOR.submit(drawable::mutate).get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
/**
* @return an array of containing versionCode and lastUpdatedTime for the package.
*/
@Thunk long[] getPackageVersion(String packageName) {
synchronized (mPackageVersions) {
long[] versions = mPackageVersions.get(packageName);
if (versions == null) {
versions = new long[2];
try {
PackageInfo info = mContext.getPackageManager().getPackageInfo(packageName,
PackageManager.GET_UNINSTALLED_PACKAGES);
versions[0] = info.versionCode;
versions[1] = info.lastUpdateTime;
} catch (NameNotFoundException e) {
Log.e(TAG, "PackageInfo not found", e);
}
mPackageVersions.put(packageName, versions);
}
return versions;
}
}
public class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap>
implements CancellationSignal.OnCancelListener {
@Thunk final WidgetCacheKey mKey;
private final WidgetItem mInfo;
private final int mPreviewHeight;
private final int mPreviewWidth;
private final WidgetCell mCaller;
private final BaseActivity mActivity;
@Thunk long[] mVersions;
@Thunk Bitmap mBitmapToRecycle;
@Nullable private Bitmap mUnusedPreviewBitmap;
private boolean mSaveToDB = false;
PreviewLoadTask(WidgetCacheKey key, WidgetItem info, int previewWidth,
int previewHeight, WidgetCell caller) {
mKey = key;
mInfo = info;
mPreviewHeight = previewHeight;
mPreviewWidth = previewWidth;
mCaller = caller;
mActivity = BaseActivity.fromContext(mCaller.getContext());
if (DEBUG) {
Log.d(TAG, String.format("%s, %s, %d, %d",
mKey, mInfo, mPreviewHeight, mPreviewWidth));
}
}
@Override
protected Bitmap doInBackground(Void... params) {
Bitmap unusedBitmap = null;
// If already cancelled before this gets to run in the background, then return early
if (isCancelled()) {
return null;
}
synchronized (mUnusedBitmaps) {
// Check if we can re-use a bitmap
for (Bitmap candidate : mUnusedBitmaps) {
if (candidate != null && candidate.isMutable() &&
candidate.getWidth() == mPreviewWidth &&
candidate.getHeight() == mPreviewHeight) {
unusedBitmap = candidate;
mUnusedBitmaps.remove(unusedBitmap);
break;
}
}
}
// creating a bitmap is expensive. Do not do this inside synchronized block.
if (unusedBitmap == null) {
unusedBitmap = Bitmap.createBitmap(mPreviewWidth, mPreviewHeight, Config.ARGB_8888);
}
// If cancelled now, don't bother reading the preview from the DB
if (isCancelled()) {
return unusedBitmap;
}
Bitmap preview = readFromDb(mKey, unusedBitmap, this);
// Only consider generating the preview if we have not cancelled the task already
if (!isCancelled() && preview == null) {
// Fetch the version info before we generate the preview, so that, in-case the
// app was updated while we are generating the preview, we use the old version info,
// which would gets re-written next time.
boolean persistable = mInfo.activityInfo == null
|| mInfo.activityInfo.isPersistable();
mVersions = persistable ? getPackageVersion(mKey.componentName.getPackageName())
: null;
// it's not in the db... we need to generate it
Pair<Bitmap, Boolean> pair = generatePreview(mActivity, mInfo, unusedBitmap,
mPreviewWidth, mPreviewHeight);
preview = pair.first;
if (preview != unusedBitmap) {
mUnusedPreviewBitmap = unusedBitmap;
}
this.mSaveToDB = pair.second;
}
return preview;
}
@Override
protected void onPostExecute(final Bitmap preview) {
mCaller.applyPreview(preview);
// Write the generated preview to the DB in the worker thread
if (mVersions != null) {
MODEL_EXECUTOR.post(new Runnable() {
@Override
public void run() {
if (mUnusedPreviewBitmap != null) {
// If we didn't end up using the bitmap, it can be added back into the
// recycled set.
synchronized (mUnusedBitmaps) {
mUnusedBitmaps.add(mUnusedPreviewBitmap);
}
}
if (!isCancelled() && mSaveToDB) {
// If we are still using this preview, then write it to the DB and then
// let the normal clear mechanism recycle the bitmap
writeToDb(mKey, mVersions, preview);
mBitmapToRecycle = preview;
} else {
// If we've already cancelled, then skip writing the bitmap to the DB
// and manually add the bitmap back to the recycled set
synchronized (mUnusedBitmaps) {
mUnusedBitmaps.add(preview);
}
}
}
});
} else {
// If we don't need to write to disk, then ensure the preview gets recycled by
// the normal clear mechanism
mBitmapToRecycle = preview;
}
}
@Override
protected void onCancelled(final Bitmap preview) {
// If we've cancelled while the task is running, then can return the bitmap to the
// recycled set immediately. Otherwise, it will be recycled after the preview is written
// to disk.
if (preview != null) {
MODEL_EXECUTOR.post(new Runnable() {
@Override
public void run() {
synchronized (mUnusedBitmaps) {
mUnusedBitmaps.add(preview);
}
}
});
}
}
@Override
public void onCancel() {
cancel(true);
// This only handles the case where the PreviewLoadTask is cancelled after the task has
// successfully completed (including having written to disk when necessary). In the
// other cases where it is cancelled while the task is running, it will be cleaned up
// in the tasks's onCancelled() call, and if cancelled while the task is writing to
// disk, it will be cancelled in the task's onPostExecute() call.
if (mBitmapToRecycle != null) {
MODEL_EXECUTOR.post(new Runnable() {
@Override
public void run() {
synchronized (mUnusedBitmaps) {
mUnusedBitmaps.add(mBitmapToRecycle);
}
mBitmapToRecycle = null;
}
});
}
}
}
private static final class WidgetCacheKey extends ComponentKey {
@Thunk final String size;
public WidgetCacheKey(ComponentName componentName, UserHandle user, String size) {
super(componentName, user);
this.size = size;
}
@Override
public int hashCode() {
return super.hashCode() ^ size.hashCode();
}
@Override
public boolean equals(Object o) {
return super.equals(o) && ((WidgetCacheKey) o).size.equals(size);
}
}
}