summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArtem Shvadskiy <ashvadskiy@cyngn.com>2016-05-03 22:40:57 (GMT)
committerArtem Shvadskiy <ashvadskiy@cyngn.com>2016-05-04 17:56:43 (GMT)
commit058ef981510ce35ca6e7f39be57a8d76c4865575 (patch)
tree96462c43961135f699eb308f3377c926480a7201
parentf271d1a1ec8ce65799db1de9adada504557af577 (diff)
downloadandroid_packages_apps_Trebuchet-058ef981510ce35ca6e7f39be57a8d76c4865575.zip
android_packages_apps_Trebuchet-058ef981510ce35ca6e7f39be57a8d76c4865575.tar.gz
android_packages_apps_Trebuchet-058ef981510ce35ca6e7f39be57a8d76c4865575.tar.bz2
Prevent widget previews from showing empty images.
When scrolling through the widget drawer, we submit multiple AsyncTasks to load and display preview images. On certain devices, attempting to load these images from AppWidgetManagerCompat (when we are generating previews for the first time) on a multi-threaded executor can cause us to receive empty images. To avoid this, we allow preview loading from the cache on a multi-threaded executor, but defer preview generation to a single-threaded executor. Additionally, the read and write db methods were not using the same ComponentName output (flattenToString vs flattenToSimpleString), which was resulting in consistent cache misses that forced unnecessary preview regeneration. This has been unified so we properly load from the cache. Change-Id: I3a90cf88fed531713e5d2df876f4ede822f7d569 issue-id: FEIJ-346 (cherry picked from commit dd6f2a2891a8445591be3e9fa53db293d7fd880b)
-rw-r--r--src/com/android/launcher3/WidgetPreviewLoader.java238
-rw-r--r--src/com/android/launcher3/widget/WidgetCell.java6
-rw-r--r--src/com/android/launcher3/widget/WidgetsContainerView.java2
3 files changed, 166 insertions, 80 deletions
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java
index 056bdec..f1eb062 100644
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ b/src/com/android/launcher3/WidgetPreviewLoader.java
@@ -176,14 +176,13 @@ public class WidgetPreviewLoader {
@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_COMPONENT, key.componentName.flattenToString());
values.put(CacheDb.COLUMN_USER, mUserManager.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, Utilities.flattenBitmap(preview));
-
try {
mDb.getWritableDatabase().insertWithOnConflict(CacheDb.TABLE_NAME, null, values,
SQLiteDatabase.CONFLICT_REPLACE);
@@ -332,18 +331,21 @@ public class WidgetPreviewLoader {
}
@Thunk Bitmap generatePreview(Launcher launcher, Object info, Bitmap recycle,
- int previewWidth, int previewHeight) {
+ int previewWidth, int previewHeight, PreviewGenerateTask task) {
if (info instanceof LauncherAppWidgetProviderInfo) {
return generateWidgetPreview(launcher, (LauncherAppWidgetProviderInfo) info,
- previewWidth, recycle, null);
+ previewWidth, recycle, null, task);
} else {
return generateShortcutPreview(launcher,
- (ResolveInfo) info, previewWidth, previewHeight, recycle);
+ (ResolveInfo) info, previewWidth, previewHeight, recycle, task);
}
}
public Bitmap generateWidgetPreview(Launcher launcher, LauncherAppWidgetProviderInfo info,
- int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) {
+ int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut, PreviewGenerateTask task) {
+ // Periodically check to bail out early.
+ if (task != null && task.isCancelled()) return null;
+
// Load the preview image if possible
if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE;
@@ -358,6 +360,9 @@ public class WidgetPreviewLoader {
}
}
+ // Periodically check to bail out early.
+ if (task != null && task.isCancelled()) return null;
+
final boolean widgetPreviewExists = (drawable != null);
final int spanX = info.spanX;
final int spanY = info.spanY;
@@ -402,6 +407,9 @@ public class WidgetPreviewLoader {
c.drawColor(0, PorterDuff.Mode.CLEAR);
}
+ // Periodically check to bail out early.
+ if (task != null && task.isCancelled()) return null;
+
// Draw the scaled preview into the final bitmap
int x = (preview.getWidth() - previewWidth) / 2;
if (widgetPreviewExists) {
@@ -434,6 +442,9 @@ public class WidgetPreviewLoader {
int smallestSide = Math.min(previewWidth, previewHeight);
float iconScale = Math.min((float) smallestSide / (appIconSize + 2 * minOffset), scale);
+ // Periodically check to bail out early.
+ if (task != null && task.isCancelled()) return null;
+
try {
Drawable icon = mutateOnMainThread(mManager.loadIcon(info, mIconCache));
if (icon != null) {
@@ -451,8 +462,11 @@ public class WidgetPreviewLoader {
return mManager.getBadgeBitmap(info, preview, imageHeight);
}
- private Bitmap generateShortcutPreview(
- Launcher launcher, ResolveInfo info, int maxWidth, int maxHeight, Bitmap preview) {
+ private Bitmap generateShortcutPreview(Launcher launcher, ResolveInfo info, int maxWidth,
+ int maxHeight, Bitmap preview, PreviewGenerateTask task) {
+ // Periodically check to bail out early.
+ if (task != null && task.isCancelled()) return null;
+
final Canvas c = new Canvas();
if (preview == null) {
preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888);
@@ -465,6 +479,9 @@ public class WidgetPreviewLoader {
c.drawColor(0, PorterDuff.Mode.CLEAR);
}
+ // Periodically check to bail out early.
+ if (task != null && task.isCancelled()) return null;
+
Drawable icon = mutateOnMainThread(mIconCache.getFullResIcon(info.activityInfo));
icon.setFilterBitmap(true);
@@ -483,6 +500,9 @@ public class WidgetPreviewLoader {
paddingLeft + scaledIconWidth, paddingTop + scaledIconWidth);
icon.draw(c);
+ // Periodically check to bail out early.
+ if (task != null && task.isCancelled()) return null;
+
// Draw the final icon at top left corner.
// TODO: use top right for RTL
int appIconSize = launcher.getDeviceProfile().iconSizePx;
@@ -537,48 +557,44 @@ public class WidgetPreviewLoader {
* A request Id which can be used by the client to cancel any request.
*/
public class PreviewLoadRequest {
-
- @Thunk final PreviewLoadTask mTask;
+ @Thunk PreviewLoadTask mLoadTask;
+ @Thunk PreviewGenerateTask mGenerateTask;
public PreviewLoadRequest(PreviewLoadTask task) {
- mTask = task;
+ mLoadTask = task;
}
public void cleanup() {
- if (mTask != null) {
- mTask.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 (mTask.mBitmapToRecycle != null) {
- mWorkerHandler.post(new Runnable() {
- @Override
- public void run() {
- synchronized (mUnusedBitmaps) {
- mUnusedBitmaps.add(mTask.mBitmapToRecycle);
- }
- mTask.mBitmapToRecycle = null;
- }
- });
+ mLoadTask.cancel(true);
+
+ if (mGenerateTask != null) {
+ mGenerateTask.cancel(true);
+
+ // This only handles the case where the PreviewGenerateTask is cancelled after the
+ // task has successfully completed. 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 (mGenerateTask.mBitmapToRecycle != null) {
+ recycleBitmap(mGenerateTask.mBitmapToRecycle);
+ }
}
}
}
- public class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap> {
+ /**
+ * Parent task to consolidate common data.
+ */
+ public abstract class PreviewTask extends AsyncTask<Void, Void, Bitmap> {
@Thunk final WidgetCacheKey mKey;
- private final Object mInfo;
- private final int mPreviewHeight;
- private final int mPreviewWidth;
- private final WidgetCell mCaller;
- @Thunk long[] mVersions;
- @Thunk Bitmap mBitmapToRecycle;
-
- PreviewLoadTask(WidgetCacheKey key, Object info, int previewWidth,
- int previewHeight, WidgetCell caller) {
+ protected final Object mInfo;
+ protected final int mPreviewHeight;
+ protected final int mPreviewWidth;
+ protected final WidgetCell mCaller;
+ protected Bitmap mUnusedBitmap;
+
+ PreviewTask(WidgetCacheKey key, Object info, int previewWidth,
+ int previewHeight, WidgetCell caller) {
mKey = key;
mInfo = info;
mPreviewHeight = previewHeight;
@@ -589,55 +605,112 @@ public class WidgetPreviewLoader {
mKey, mInfo, mPreviewHeight, mPreviewWidth));
}
}
+ }
+
+ /**
+ * This task's job is to load the preview if it is ready in the database. It will not generate
+ * a preview on it's own, because this tasks runs on a multi-threaded executor,
+ * and non-synchronous loading from {@link AppWidgetManagerCompat} can return null images.
+ */
+ public class PreviewLoadTask extends PreviewTask {
+ PreviewLoadTask(WidgetCacheKey key, Object info, int previewWidth,
+ int previewHeight, WidgetCell caller) {
+ super(key, info, previewWidth, previewHeight, caller);
+ }
@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;
- }
+ 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);
+ mUnusedBitmap = candidate;
+ mUnusedBitmaps.remove(mUnusedBitmap);
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 (mUnusedBitmap == null) {
+ mUnusedBitmap = 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.
- mVersions = getPackageVersion(mKey.componentName.getPackageName());
-
- Launcher launcher = (Launcher) mCaller.getContext();
-
- // it's not in the db... we need to generate it
- preview = generatePreview(launcher, mInfo, unusedBitmap, mPreviewWidth, mPreviewHeight);
+ if (isCancelled()) return null;
+
+ return readFromDb(mKey, mUnusedBitmap, this);
+ }
+
+ @Override
+ protected void onPostExecute(final Bitmap preview) {
+ if (isCancelled()) return;
+
+ // We need to generate a new image
+ if (preview == null) {
+ // Make sure we are still a valid request.
+ PreviewLoadRequest request = mCaller.getActiveRequest();
+ if (request == null || request.mLoadTask != this) return;
+
+ request.mGenerateTask = new PreviewGenerateTask(mKey, mInfo, mPreviewWidth,
+ mPreviewHeight, mCaller, mUnusedBitmap);
+
+ // Must be run on default synchronous executor, otherwise images may fail to load.
+ request.mGenerateTask.execute();
+ } else {
+ applyPreviewIfActive(this, mCaller, preview);
}
- return preview;
+ }
+
+ @Override
+ protected void onCancelled(final Bitmap preview) {
+ recycleBitmap(preview);
+ }
+ }
+
+ /**
+ * This task's job is to generate preview images if they are not currently available in
+ * the database. It must be run on a single threaded worker, otherwise
+ * {@link AppWidgetManagerCompat} may return null images.
+ */
+ public class PreviewGenerateTask extends PreviewTask {
+ @Thunk long[] mVersions;
+ @Thunk Bitmap mBitmapToRecycle;
+
+ PreviewGenerateTask(WidgetCacheKey key, Object info, int previewWidth,
+ int previewHeight, WidgetCell caller, Bitmap unusedBitmap) {
+ super(key, info, previewWidth, previewHeight, caller);
+ mUnusedBitmap = unusedBitmap;
+ }
+
+ @Override
+ protected Bitmap doInBackground(Void... params) {
+ if (isCancelled()) return 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.
+ mVersions = getPackageVersion(mKey.componentName.getPackageName());
+
+ Launcher launcher = (Launcher) mCaller.getContext();
+
+ // Periodically check to bail out early.
+ if (isCancelled()) return null;
+
+ // it's not in the db... we need to generate it
+ return generatePreview(launcher, mInfo, mUnusedBitmap, mPreviewWidth, mPreviewHeight, this);
}
@Override
protected void onPostExecute(final Bitmap preview) {
- mCaller.applyPreview(preview);
+ if (preview == null || isCancelled()) return;
+
+ applyPreviewIfActive(this, mCaller, preview);
// Write the generated preview to the DB in the worker thread
if (mVersions != null) {
@@ -667,19 +740,28 @@ public class WidgetPreviewLoader {
@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) {
- mWorkerHandler.post(new Runnable() {
- @Override
- public void run() {
- synchronized (mUnusedBitmaps) {
- mUnusedBitmaps.add(preview);
- }
+ recycleBitmap(preview);
+ }
+ }
+
+ private void recycleBitmap(final Bitmap bitmap) {
+ if (bitmap != null) {
+ mWorkerHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mUnusedBitmaps) {
+ mUnusedBitmaps.add(bitmap);
}
- });
- }
+ }
+ });
+ }
+ }
+
+ private static void applyPreviewIfActive(PreviewTask task, WidgetCell caller, Bitmap preview) {
+ // Verify that we are still the active preview task.
+ PreviewLoadRequest request = caller.getActiveRequest();
+ if (request != null && (request.mLoadTask == task || request.mGenerateTask == task)) {
+ caller.applyPreview(preview);
}
}
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 94bbd92..761747b 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -183,7 +183,7 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener {
public void ensurePreview() {
if (mActiveRequest != null) {
- return;
+ mActiveRequest.cleanup();
}
int[] size = getPreviewSize();
if (DEBUG) {
@@ -227,4 +227,8 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener {
}
return "";
}
+
+ public PreviewLoadRequest getActiveRequest() {
+ return mActiveRequest;
+ }
}
diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java
index fff60a1..b9a9119 100644
--- a/src/com/android/launcher3/widget/WidgetsContainerView.java
+++ b/src/com/android/launcher3/widget/WidgetsContainerView.java
@@ -246,7 +246,7 @@ public class WidgetsContainerView extends BaseContainerView
int[] previewSizeBeforeScale = new int[1];
preview = getWidgetPreviewLoader().generateWidgetPreview(mLauncher,
- createWidgetInfo.info, maxWidth, null, previewSizeBeforeScale);
+ createWidgetInfo.info, maxWidth, null, previewSizeBeforeScale, null);
if (previewSizeBeforeScale[0] < icon.getWidth()) {
// The icon has extra padding around it.