blob: cb3126c4897d4da3b3d76836451555626d090bdf [file] [log] [blame]
Daniel Sandler325dc232013-06-05 22:57:57 -04001package com.android.launcher3;
Michael Jurka05713af2013-01-23 12:39:24 +01002
Michael Jurka05713af2013-01-23 12:39:24 +01003import android.content.ComponentName;
4import android.content.ContentValues;
5import android.content.Context;
Sunny Goyal4ddc4012016-03-10 12:02:29 -08006import android.content.pm.ActivityInfo;
Sunny Goyal5b0e6692015-03-19 14:31:19 -07007import android.content.pm.PackageInfo;
Sunny Goyal4ddc4012016-03-10 12:02:29 -08008import android.content.pm.PackageManager;
Sunny Goyal5b0e6692015-03-19 14:31:19 -07009import android.content.pm.PackageManager.NameNotFoundException;
Michael Jurka05713af2013-01-23 12:39:24 +010010import android.content.res.Resources;
11import android.database.Cursor;
Sunny Goyal5b0e6692015-03-19 14:31:19 -070012import android.database.SQLException;
Michael Jurka05713af2013-01-23 12:39:24 +010013import android.database.sqlite.SQLiteDatabase;
Michael Jurka05713af2013-01-23 12:39:24 +010014import android.graphics.Bitmap;
15import android.graphics.Bitmap.Config;
16import android.graphics.BitmapFactory;
17import android.graphics.Canvas;
18import android.graphics.ColorMatrix;
19import android.graphics.ColorMatrixColorFilter;
20import android.graphics.Paint;
21import android.graphics.PorterDuff;
22import android.graphics.Rect;
Sunny Goyal4cad7532015-03-18 15:56:30 -070023import android.graphics.RectF;
Michael Jurka05713af2013-01-23 12:39:24 +010024import android.graphics.drawable.BitmapDrawable;
25import android.graphics.drawable.Drawable;
26import android.os.AsyncTask;
Winson Chung05304db2015-05-18 16:53:20 -070027import android.os.Handler;
Michael Jurka05713af2013-01-23 12:39:24 +010028import android.util.Log;
Sunny Goyal5b0e6692015-03-19 14:31:19 -070029import android.util.LongSparseArray;
Sunny Goyal383c5072015-06-12 21:18:53 -070030
Sunny Goyalffe83f12014-08-14 17:39:34 -070031import com.android.launcher3.compat.AppWidgetManagerCompat;
Sunny Goyal5b0e6692015-03-19 14:31:19 -070032import com.android.launcher3.compat.UserHandleCompat;
33import com.android.launcher3.compat.UserManagerCompat;
Sunny Goyal4ddc4012016-03-10 12:02:29 -080034import com.android.launcher3.model.WidgetItem;
Sunny Goyal5b0e6692015-03-19 14:31:19 -070035import com.android.launcher3.util.ComponentKey;
Sunny Goyal6f709362015-12-17 17:09:36 -080036import com.android.launcher3.util.SQLiteCacheHelper;
Adam Cohen091440a2015-03-18 14:16:05 -070037import com.android.launcher3.util.Thunk;
Hyunyoung Song3f471442015-04-08 19:01:34 -070038import com.android.launcher3.widget.WidgetCell;
Hyunyoung Song8821ca92015-05-04 16:28:20 -070039
40import java.util.ArrayList;
Sunny Goyal5b0e6692015-03-19 14:31:19 -070041import java.util.Collections;
Michael Jurka05713af2013-01-23 12:39:24 +010042import java.util.HashMap;
43import java.util.HashSet;
Sunny Goyal5b0e6692015-03-19 14:31:19 -070044import java.util.Set;
45import java.util.WeakHashMap;
Adrian Roos65d60e22014-04-15 21:07:49 +020046import java.util.concurrent.Callable;
47import java.util.concurrent.ExecutionException;
Michael Jurka05713af2013-01-23 12:39:24 +010048
Sunny Goyalffe83f12014-08-14 17:39:34 -070049public class WidgetPreviewLoader {
Michael Jurka05713af2013-01-23 12:39:24 +010050
Sunny Goyalffe83f12014-08-14 17:39:34 -070051 private static final String TAG = "WidgetPreviewLoader";
Hyunyoung Song3f471442015-04-08 19:01:34 -070052 private static final boolean DEBUG = false;
Sunny Goyalffe83f12014-08-14 17:39:34 -070053
54 private static final float WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE = 0.25f;
Sunny Goyalffe83f12014-08-14 17:39:34 -070055
Sunny Goyal5b0e6692015-03-19 14:31:19 -070056 private final HashMap<String, long[]> mPackageVersions = new HashMap<>();
Hyunyoung Song559d90d2015-04-28 15:06:45 -070057
58 /**
59 * Weak reference objects, do not prevent their referents from being made finalizable,
60 * finalized, and then reclaimed.
Hyunyoung Songe98f4a42015-06-16 10:45:24 -070061 * Note: synchronized block used for this variable is expensive and the block should always
62 * be posted to a background thread.
Hyunyoung Song559d90d2015-04-28 15:06:45 -070063 */
Sunny Goyalb4cbea42015-06-16 15:10:36 -070064 @Thunk final Set<Bitmap> mUnusedBitmaps =
Hyunyoung Song559d90d2015-04-28 15:06:45 -070065 Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>());
Sunny Goyal4cad7532015-03-18 15:56:30 -070066
67 private final Context mContext;
Sunny Goyalffe83f12014-08-14 17:39:34 -070068 private final IconCache mIconCache;
Sunny Goyal5b0e6692015-03-19 14:31:19 -070069 private final UserManagerCompat mUserManager;
Hyunyoung Song3e840f42016-03-01 11:57:44 -080070 private final AppWidgetManagerCompat mWidgetManager;
Sunny Goyal5b0e6692015-03-19 14:31:19 -070071 private final CacheDb mDb;
Hyunyoung Song41e33692015-06-15 12:26:54 -070072 private final int mProfileBadgeMargin;
Michael Jurka05713af2013-01-23 12:39:24 +010073
Adrian Roos65d60e22014-04-15 21:07:49 +020074 private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
Sunny Goyal316490e2015-06-02 09:38:28 -070075 @Thunk final Handler mWorkerHandler;
Adrian Roos65d60e22014-04-15 21:07:49 +020076
Sunny Goyal383c5072015-06-12 21:18:53 -070077 public WidgetPreviewLoader(Context context, IconCache iconCache) {
Chris Wrenfd13c712013-09-27 15:45:19 -040078 mContext = context;
Sunny Goyal5b0e6692015-03-19 14:31:19 -070079 mIconCache = iconCache;
Hyunyoung Song3e840f42016-03-01 11:57:44 -080080 mWidgetManager = AppWidgetManagerCompat.getInstance(context);
Sunny Goyal5b0e6692015-03-19 14:31:19 -070081 mUserManager = UserManagerCompat.getInstance(context);
82 mDb = new CacheDb(context);
Winson Chung05304db2015-05-18 16:53:20 -070083 mWorkerHandler = new Handler(LauncherModel.getWorkerLooper());
Hyunyoung Song41e33692015-06-15 12:26:54 -070084 mProfileBadgeMargin = context.getResources()
85 .getDimensionPixelSize(R.dimen.profile_badge_margin);
Michael Jurka3f4e0702013-02-05 11:21:28 +010086 }
Sunny Goyalffe83f12014-08-14 17:39:34 -070087
Sunny Goyal5b0e6692015-03-19 14:31:19 -070088 /**
89 * Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be
90 * called on UI thread
91 *
Sunny Goyal5b0e6692015-03-19 14:31:19 -070092 * @return a request id which can be used to cancel the request.
93 */
Sunny Goyal4ddc4012016-03-10 12:02:29 -080094 public PreviewLoadRequest getPreview(WidgetItem item, int previewWidth,
Adam Cohen2e6da152015-05-06 11:42:25 -070095 int previewHeight, WidgetCell caller) {
Sunny Goyal5b0e6692015-03-19 14:31:19 -070096 String size = previewWidth + "x" + previewHeight;
Sunny Goyal4ddc4012016-03-10 12:02:29 -080097 WidgetCacheKey key = new WidgetCacheKey(item.componentName, item.user, size);
Michael Jurka3f4e0702013-02-05 11:21:28 +010098
Sunny Goyal4ddc4012016-03-10 12:02:29 -080099 PreviewLoadTask task = new PreviewLoadTask(key, item, previewWidth, previewHeight, caller);
Sunny Goyal8ac727b2015-09-23 15:38:09 -0700100 task.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR);
Hyunyoung Song559d90d2015-04-28 15:06:45 -0700101 return new PreviewLoadRequest(task);
Michael Jurka05713af2013-01-23 12:39:24 +0100102 }
103
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700104 /**
105 * The DB holds the generated previews for various components. Previews can also have different
106 * sizes (landscape vs portrait).
107 */
Sunny Goyal6f709362015-12-17 17:09:36 -0800108 private static class CacheDb extends SQLiteCacheHelper {
Sunny Goyal2d648b02015-08-11 13:56:28 -0700109 private static final int DB_VERSION = 4;
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700110
111 private static final String TABLE_NAME = "shortcut_and_widget_previews";
112 private static final String COLUMN_COMPONENT = "componentName";
113 private static final String COLUMN_USER = "profileId";
114 private static final String COLUMN_SIZE = "size";
115 private static final String COLUMN_PACKAGE = "packageName";
116 private static final String COLUMN_LAST_UPDATED = "lastUpdated";
117 private static final String COLUMN_VERSION = "version";
118 private static final String COLUMN_PREVIEW_BITMAP = "preview_bitmap";
Michael Jurka05713af2013-01-23 12:39:24 +0100119
Michael Jurkad9cb4a12013-03-19 12:01:06 +0100120 public CacheDb(Context context) {
Sunny Goyal6f709362015-12-17 17:09:36 -0800121 super(context, LauncherFiles.WIDGET_PREVIEWS_DB, DB_VERSION, TABLE_NAME);
Michael Jurka05713af2013-01-23 12:39:24 +0100122 }
123
124 @Override
Sunny Goyal6f709362015-12-17 17:09:36 -0800125 public void onCreateTable(SQLiteDatabase database) {
Michael Jurka32b7a092013-02-07 20:06:49 +0100126 database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700127 COLUMN_COMPONENT + " TEXT NOT NULL, " +
128 COLUMN_USER + " INTEGER NOT NULL, " +
Michael Jurka05713af2013-01-23 12:39:24 +0100129 COLUMN_SIZE + " TEXT NOT NULL, " +
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700130 COLUMN_PACKAGE + " TEXT NOT NULL, " +
131 COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " +
132 COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " +
133 COLUMN_PREVIEW_BITMAP + " BLOB, " +
134 "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ", " + COLUMN_SIZE + ") " +
Michael Jurka32b7a092013-02-07 20:06:49 +0100135 ");");
Michael Jurka05713af2013-01-23 12:39:24 +0100136 }
Michael Jurka05713af2013-01-23 12:39:24 +0100137 }
138
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700139 @Thunk void writeToDb(WidgetCacheKey key, long[] versions, Bitmap preview) {
Michael Jurka05713af2013-01-23 12:39:24 +0100140 ContentValues values = new ContentValues();
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700141 values.put(CacheDb.COLUMN_COMPONENT, key.componentName.flattenToShortString());
142 values.put(CacheDb.COLUMN_USER, mUserManager.getSerialNumberForUser(key.user));
143 values.put(CacheDb.COLUMN_SIZE, key.size);
144 values.put(CacheDb.COLUMN_PACKAGE, key.componentName.getPackageName());
145 values.put(CacheDb.COLUMN_VERSION, versions[0]);
146 values.put(CacheDb.COLUMN_LAST_UPDATED, versions[1]);
147 values.put(CacheDb.COLUMN_PREVIEW_BITMAP, Utilities.flattenBitmap(preview));
Sunny Goyal6f709362015-12-17 17:09:36 -0800148 mDb.insertOrReplace(values);
Michael Jurka05713af2013-01-23 12:39:24 +0100149 }
150
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700151 public void removePackage(String packageName, UserHandleCompat user) {
152 removePackage(packageName, user, mUserManager.getSerialNumberForUser(user));
153 }
154
155 private void removePackage(String packageName, UserHandleCompat user, long userSerial) {
156 synchronized(mPackageVersions) {
157 mPackageVersions.remove(packageName);
158 }
159
Sunny Goyal6f709362015-12-17 17:09:36 -0800160 mDb.delete(
161 CacheDb.COLUMN_PACKAGE + " = ? AND " + CacheDb.COLUMN_USER + " = ?",
162 new String[]{packageName, Long.toString(userSerial)});
Michael Jurka8ff02ca2013-11-01 14:19:27 +0100163 }
164
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700165 /**
166 * Updates the persistent DB:
167 * 1. Any preview generated for an old package version is removed
168 * 2. Any preview for an absent package is removed
169 * This ensures that we remove entries for packages which changed while the launcher was dead.
170 */
Sunny Goyal4ddc4012016-03-10 12:02:29 -0800171 public void removeObsoletePreviews(ArrayList<? extends ComponentKey> list) {
Hyunyoung Song2bd3d7d2015-05-21 13:04:53 -0700172 Utilities.assertWorkerThread();
Hyunyoung Song8821ca92015-05-04 16:28:20 -0700173
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700174 LongSparseArray<HashSet<String>> validPackages = new LongSparseArray<>();
175
Sunny Goyal4ddc4012016-03-10 12:02:29 -0800176 for (ComponentKey key : list) {
177 final long userId = mUserManager.getSerialNumberForUser(key.user);
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700178 HashSet<String> packages = validPackages.get(userId);
179 if (packages == null) {
180 packages = new HashSet<>();
181 validPackages.put(userId, packages);
182 }
Sunny Goyal4ddc4012016-03-10 12:02:29 -0800183 packages.add(key.componentName.getPackageName());
Michael Jurka05713af2013-01-23 12:39:24 +0100184 }
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700185
186 LongSparseArray<HashSet<String>> packagesToDelete = new LongSparseArray<>();
187 Cursor c = null;
188 try {
Sunny Goyal6f709362015-12-17 17:09:36 -0800189 c = mDb.query(
190 new String[]{CacheDb.COLUMN_USER, CacheDb.COLUMN_PACKAGE,
191 CacheDb.COLUMN_LAST_UPDATED, CacheDb.COLUMN_VERSION},
192 null, null);
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700193 while (c.moveToNext()) {
194 long userId = c.getLong(0);
195 String pkg = c.getString(1);
196 long lastUpdated = c.getLong(2);
197 long version = c.getLong(3);
198
199 HashSet<String> packages = validPackages.get(userId);
200 if (packages != null && packages.contains(pkg)) {
201 long[] versions = getPackageVersion(pkg);
202 if (versions[0] == version && versions[1] == lastUpdated) {
203 // Every thing checks out
204 continue;
205 }
206 }
207
208 // We need to delete this package.
209 packages = packagesToDelete.get(userId);
210 if (packages == null) {
211 packages = new HashSet<>();
212 packagesToDelete.put(userId, packages);
213 }
214 packages.add(pkg);
215 }
216
217 for (int i = 0; i < packagesToDelete.size(); i++) {
218 long userId = packagesToDelete.keyAt(i);
219 UserHandleCompat user = mUserManager.getUserForSerialNumber(userId);
220 for (String pkg : packagesToDelete.valueAt(i)) {
221 removePackage(pkg, user, userId);
222 }
223 }
224 } catch (SQLException e) {
Sunny Goyal6f709362015-12-17 17:09:36 -0800225 Log.e(TAG, "Error updating widget previews", e);
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700226 } finally {
227 if (c != null) {
228 c.close();
229 }
230 }
231 }
232
Winson Chung05304db2015-05-18 16:53:20 -0700233 /**
234 * Reads the preview bitmap from the DB or null if the preview is not in the DB.
235 */
Sunny Goyal316490e2015-06-02 09:38:28 -0700236 @Thunk Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask) {
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700237 Cursor cursor = null;
238 try {
Sunny Goyal6f709362015-12-17 17:09:36 -0800239 cursor = mDb.query(
240 new String[]{CacheDb.COLUMN_PREVIEW_BITMAP},
241 CacheDb.COLUMN_COMPONENT + " = ? AND " + CacheDb.COLUMN_USER + " = ? AND "
242 + CacheDb.COLUMN_SIZE + " = ?",
243 new String[]{
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700244 key.componentName.flattenToString(),
245 Long.toString(mUserManager.getSerialNumberForUser(key.user)),
246 key.size
Sunny Goyal6f709362015-12-17 17:09:36 -0800247 });
Winson Chung05304db2015-05-18 16:53:20 -0700248 // If cancelled, skip getting the blob and decoding it into a bitmap
249 if (loadTask.isCancelled()) {
250 return null;
251 }
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700252 if (cursor.moveToNext()) {
253 byte[] blob = cursor.getBlob(0);
254 BitmapFactory.Options opts = new BitmapFactory.Options();
255 opts.inBitmap = recycle;
Michael Jurka6e27f642013-12-10 13:40:30 +0100256 try {
Winson Chung05304db2015-05-18 16:53:20 -0700257 if (!loadTask.isCancelled()) {
258 return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts);
259 }
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700260 } catch (Exception e) {
261 return null;
Michael Jurka6e27f642013-12-10 13:40:30 +0100262 }
Michael Jurka05713af2013-01-23 12:39:24 +0100263 }
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700264 } catch (SQLException e) {
265 Log.w(TAG, "Error loading preview from DB", e);
266 } finally {
267 if (cursor != null) {
268 cursor.close();
269 }
270 }
271 return null;
Michael Jurka05713af2013-01-23 12:39:24 +0100272 }
273
Sunny Goyal4ddc4012016-03-10 12:02:29 -0800274 private Bitmap generatePreview(Launcher launcher, WidgetItem item, Bitmap recycle,
Adam Cohen2e6da152015-05-06 11:42:25 -0700275 int previewWidth, int previewHeight) {
Sunny Goyal4ddc4012016-03-10 12:02:29 -0800276 if (item.widgetInfo != null) {
277 return generateWidgetPreview(launcher, item.widgetInfo,
Adam Cohen2e6da152015-05-06 11:42:25 -0700278 previewWidth, recycle, null);
Michael Jurka05713af2013-01-23 12:39:24 +0100279 } else {
Sunny Goyal4ddc4012016-03-10 12:02:29 -0800280 return generateShortcutPreview(launcher, item.activityInfo,
281 previewWidth, previewHeight, recycle);
Michael Jurka05713af2013-01-23 12:39:24 +0100282 }
283 }
284
Hyunyoung Song3e840f42016-03-01 11:57:44 -0800285 /**
286 * Generates the widget preview from either the {@link AppWidgetManagerCompat} or cache
287 * and add badge at the bottom right corner.
288 *
289 * @param launcher
290 * @param info information about the widget
291 * @param maxPreviewWidth width of the preview on either workspace or tray
292 * @param preview bitmap that can be recycled
293 * @param preScaledWidthOut return the width of the returned bitmap
294 * @return
295 */
Adam Cohen2e6da152015-05-06 11:42:25 -0700296 public Bitmap generateWidgetPreview(Launcher launcher, LauncherAppWidgetProviderInfo info,
Sunny Goyal4cad7532015-03-18 15:56:30 -0700297 int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) {
Michael Jurka05713af2013-01-23 12:39:24 +0100298 // Load the preview image if possible
Michael Jurka05713af2013-01-23 12:39:24 +0100299 if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE;
Michael Jurka05713af2013-01-23 12:39:24 +0100300
301 Drawable drawable = null;
Sunny Goyalffe83f12014-08-14 17:39:34 -0700302 if (info.previewImage != 0) {
Hyunyoung Song3e840f42016-03-01 11:57:44 -0800303 drawable = mWidgetManager.loadPreview(info);
Adrian Roosfa9ffc22014-05-12 15:59:59 +0200304 if (drawable != null) {
305 drawable = mutateOnMainThread(drawable);
306 } else {
Michael Jurka05713af2013-01-23 12:39:24 +0100307 Log.w(TAG, "Can't load widget preview drawable 0x" +
Sunny Goyalffe83f12014-08-14 17:39:34 -0700308 Integer.toHexString(info.previewImage) + " for provider: " + info.provider);
Michael Jurka05713af2013-01-23 12:39:24 +0100309 }
310 }
311
Sunny Goyal4cad7532015-03-18 15:56:30 -0700312 final boolean widgetPreviewExists = (drawable != null);
Sunny Goyal233ee962015-08-03 13:05:01 -0700313 final int spanX = info.spanX;
314 final int spanY = info.spanY;
Sunny Goyal4cad7532015-03-18 15:56:30 -0700315
Michael Jurka05713af2013-01-23 12:39:24 +0100316 int previewWidth;
317 int previewHeight;
Hyunyoung Song3e840f42016-03-01 11:57:44 -0800318
Sunny Goyal4cad7532015-03-18 15:56:30 -0700319 Bitmap tileBitmap = null;
320
Michael Jurka05713af2013-01-23 12:39:24 +0100321 if (widgetPreviewExists) {
322 previewWidth = drawable.getIntrinsicWidth();
323 previewHeight = drawable.getIntrinsicHeight();
324 } else {
325 // Generate a preview image if we couldn't load one
Sunny Goyal4cad7532015-03-18 15:56:30 -0700326 tileBitmap = ((BitmapDrawable) mContext.getResources().getDrawable(
327 R.drawable.widget_tile)).getBitmap();
328 previewWidth = tileBitmap.getWidth() * spanX;
329 previewHeight = tileBitmap.getHeight() * spanY;
Michael Jurka05713af2013-01-23 12:39:24 +0100330 }
331
332 // Scale to fit width only - let the widget preview be clipped in the
333 // vertical dimension
334 float scale = 1f;
335 if (preScaledWidthOut != null) {
336 preScaledWidthOut[0] = previewWidth;
337 }
338 if (previewWidth > maxPreviewWidth) {
Hyunyoung Songb9f932e2015-07-30 15:04:59 -0700339 scale = (maxPreviewWidth - 2 * mProfileBadgeMargin) / (float) (previewWidth);
Michael Jurka05713af2013-01-23 12:39:24 +0100340 }
341 if (scale != 1f) {
342 previewWidth = (int) (scale * previewWidth);
343 previewHeight = (int) (scale * previewHeight);
344 }
345
346 // If a bitmap is passed in, we use it; otherwise, we create a bitmap of the right size
Sunny Goyal4cad7532015-03-18 15:56:30 -0700347 final Canvas c = new Canvas();
Michael Jurka05713af2013-01-23 12:39:24 +0100348 if (preview == null) {
349 preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);
Sunny Goyal4cad7532015-03-18 15:56:30 -0700350 c.setBitmap(preview);
351 } else {
352 // Reusing bitmap. Clear it.
353 c.setBitmap(preview);
354 c.drawColor(0, PorterDuff.Mode.CLEAR);
Michael Jurka05713af2013-01-23 12:39:24 +0100355 }
356
357 // Draw the scaled preview into the final bitmap
Hyunyoung Songb9f932e2015-07-30 15:04:59 -0700358 int x = (preview.getWidth() - previewWidth) / 2;
Michael Jurka05713af2013-01-23 12:39:24 +0100359 if (widgetPreviewExists) {
Sunny Goyal4cad7532015-03-18 15:56:30 -0700360 drawable.setBounds(x, 0, x + previewWidth, previewHeight);
361 drawable.draw(c);
Michael Jurka05713af2013-01-23 12:39:24 +0100362 } else {
Sunny Goyal4cad7532015-03-18 15:56:30 -0700363 final Paint p = new Paint();
364 p.setFilterBitmap(true);
Adam Cohen2e6da152015-05-06 11:42:25 -0700365 int appIconSize = launcher.getDeviceProfile().iconSizePx;
Michael Jurka05713af2013-01-23 12:39:24 +0100366
Sunny Goyal4cad7532015-03-18 15:56:30 -0700367 // draw the spanX x spanY tiles
368 final Rect src = new Rect(0, 0, tileBitmap.getWidth(), tileBitmap.getHeight());
369
370 float tileW = scale * tileBitmap.getWidth();
371 float tileH = scale * tileBitmap.getHeight();
372 final RectF dst = new RectF(0, 0, tileW, tileH);
373
374 float tx = x;
375 for (int i = 0; i < spanX; i++, tx += tileW) {
376 float ty = 0;
377 for (int j = 0; j < spanY; j++, ty += tileH) {
378 dst.offsetTo(tx, ty);
379 c.drawBitmap(tileBitmap, src, dst, p);
380 }
Michael Jurka05713af2013-01-23 12:39:24 +0100381 }
Sunny Goyal4cad7532015-03-18 15:56:30 -0700382
383 // Draw the icon in the top left corner
384 // TODO: use top right for RTL
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700385 int minOffset = (int) (appIconSize * WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE);
Sunny Goyal4cad7532015-03-18 15:56:30 -0700386 int smallestSide = Math.min(previewWidth, previewHeight);
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700387 float iconScale = Math.min((float) smallestSide / (appIconSize + 2 * minOffset), scale);
Sunny Goyal4cad7532015-03-18 15:56:30 -0700388
389 try {
Hyunyoung Song3e840f42016-03-01 11:57:44 -0800390 Drawable icon = mWidgetManager.loadIcon(info, mIconCache);
Sunny Goyal4cad7532015-03-18 15:56:30 -0700391 if (icon != null) {
Sunny Goyal1ba7e362015-10-26 10:42:12 -0700392 icon = mutateOnMainThread(icon);
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700393 int hoffset = (int) ((tileW - appIconSize * iconScale) / 2) + x;
394 int yoffset = (int) ((tileH - appIconSize * iconScale) / 2);
Sunny Goyal4cad7532015-03-18 15:56:30 -0700395 icon.setBounds(hoffset, yoffset,
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700396 hoffset + (int) (appIconSize * iconScale),
397 yoffset + (int) (appIconSize * iconScale));
Sunny Goyal4cad7532015-03-18 15:56:30 -0700398 icon.draw(c);
399 }
Hyunyoung Song3e840f42016-03-01 11:57:44 -0800400 } catch (Resources.NotFoundException e) {
401 }
Michael Jurka05713af2013-01-23 12:39:24 +0100402 c.setBitmap(null);
403 }
Hyunyoung Song3e840f42016-03-01 11:57:44 -0800404 int imageWidth = Math.min(preview.getWidth(), previewWidth + mProfileBadgeMargin);
Hyunyoung Song41e33692015-06-15 12:26:54 -0700405 int imageHeight = Math.min(preview.getHeight(), previewHeight + mProfileBadgeMargin);
Hyunyoung Song3e840f42016-03-01 11:57:44 -0800406 return mWidgetManager.getBadgeBitmap(info, preview, imageWidth, imageHeight);
Michael Jurka05713af2013-01-23 12:39:24 +0100407 }
408
409 private Bitmap generateShortcutPreview(
Sunny Goyal4ddc4012016-03-10 12:02:29 -0800410 Launcher launcher, ActivityInfo info, int maxWidth, int maxHeight, Bitmap preview) {
Sunny Goyal4cad7532015-03-18 15:56:30 -0700411 final Canvas c = new Canvas();
412 if (preview == null) {
Michael Jurka05713af2013-01-23 12:39:24 +0100413 preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888);
Sunny Goyal4cad7532015-03-18 15:56:30 -0700414 c.setBitmap(preview);
415 } else if (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight) {
416 throw new RuntimeException("Improperly sized bitmap passed as argument");
417 } else {
418 // Reusing bitmap. Clear it.
419 c.setBitmap(preview);
420 c.drawColor(0, PorterDuff.Mode.CLEAR);
Michael Jurka05713af2013-01-23 12:39:24 +0100421 }
422
Sunny Goyal4ddc4012016-03-10 12:02:29 -0800423 Drawable icon = mutateOnMainThread(mIconCache.getFullResIcon(info));
Sunny Goyal4cad7532015-03-18 15:56:30 -0700424 icon.setFilterBitmap(true);
425
Michael Jurka05713af2013-01-23 12:39:24 +0100426 // Draw a desaturated/scaled version of the icon in the background as a watermark
Sunny Goyal4cad7532015-03-18 15:56:30 -0700427 ColorMatrix colorMatrix = new ColorMatrix();
428 colorMatrix.setSaturation(0);
429 icon.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
430 icon.setAlpha((int) (255 * 0.06f));
431
432 Resources res = mContext.getResources();
433 int paddingTop = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top);
434 int paddingLeft = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left);
435 int paddingRight = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right);
436 int scaledIconWidth = (maxWidth - paddingLeft - paddingRight);
437 icon.setBounds(paddingLeft, paddingTop,
438 paddingLeft + scaledIconWidth, paddingTop + scaledIconWidth);
439 icon.draw(c);
440
441 // Draw the final icon at top left corner.
442 // TODO: use top right for RTL
Adam Cohen2e6da152015-05-06 11:42:25 -0700443 int appIconSize = launcher.getDeviceProfile().iconSizePx;
444
Sunny Goyal4cad7532015-03-18 15:56:30 -0700445 icon.setAlpha(255);
446 icon.setColorFilter(null);
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700447 icon.setBounds(0, 0, appIconSize, appIconSize);
Sunny Goyal4cad7532015-03-18 15:56:30 -0700448 icon.draw(c);
449
Michael Jurka05713af2013-01-23 12:39:24 +0100450 c.setBitmap(null);
Michael Jurka05713af2013-01-23 12:39:24 +0100451 return preview;
452 }
453
Adrian Roos65d60e22014-04-15 21:07:49 +0200454 private Drawable mutateOnMainThread(final Drawable drawable) {
455 try {
456 return mMainThreadExecutor.submit(new Callable<Drawable>() {
457 @Override
458 public Drawable call() throws Exception {
459 return drawable.mutate();
460 }
461 }).get();
462 } catch (InterruptedException e) {
463 Thread.currentThread().interrupt();
464 throw new RuntimeException(e);
465 } catch (ExecutionException e) {
466 throw new RuntimeException(e);
467 }
468 }
Adrian Roos1f375ab2014-04-28 18:26:38 +0200469
Adrian Roos1f375ab2014-04-28 18:26:38 +0200470 /**
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700471 * @return an array of containing versionCode and lastUpdatedTime for the package.
Adrian Roos1f375ab2014-04-28 18:26:38 +0200472 */
Sunny Goyal316490e2015-06-02 09:38:28 -0700473 @Thunk long[] getPackageVersion(String packageName) {
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700474 synchronized (mPackageVersions) {
475 long[] versions = mPackageVersions.get(packageName);
476 if (versions == null) {
477 versions = new long[2];
Adrian Roos1f375ab2014-04-28 18:26:38 +0200478 try {
Sunny Goyal4ddc4012016-03-10 12:02:29 -0800479 PackageInfo info = mContext.getPackageManager().getPackageInfo(packageName,
480 PackageManager.GET_UNINSTALLED_PACKAGES);
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700481 versions[0] = info.versionCode;
482 versions[1] = info.lastUpdateTime;
483 } catch (NameNotFoundException e) {
484 Log.e(TAG, "PackageInfo not found", e);
485 }
486 mPackageVersions.put(packageName, versions);
487 }
488 return versions;
489 }
490 }
491
492 /**
493 * A request Id which can be used by the client to cancel any request.
494 */
495 public class PreviewLoadRequest {
496
Sunny Goyalb4cbea42015-06-16 15:10:36 -0700497 @Thunk final PreviewLoadTask mTask;
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700498
Hyunyoung Song559d90d2015-04-28 15:06:45 -0700499 public PreviewLoadRequest(PreviewLoadTask task) {
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700500 mTask = task;
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700501 }
502
Hyunyoung Song559d90d2015-04-28 15:06:45 -0700503 public void cleanup() {
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700504 if (mTask != null) {
505 mTask.cancel(true);
506 }
507
Winson Chung05304db2015-05-18 16:53:20 -0700508 // This only handles the case where the PreviewLoadTask is cancelled after the task has
509 // successfully completed (including having written to disk when necessary). In the
510 // other cases where it is cancelled while the task is running, it will be cleaned up
511 // in the tasks's onCancelled() call, and if cancelled while the task is writing to
512 // disk, it will be cancelled in the task's onPostExecute() call.
513 if (mTask.mBitmapToRecycle != null) {
Hyunyoung Songe98f4a42015-06-16 10:45:24 -0700514 mWorkerHandler.post(new Runnable() {
515 @Override
516 public void run() {
517 synchronized (mUnusedBitmaps) {
518 mUnusedBitmaps.add(mTask.mBitmapToRecycle);
519 }
520 mTask.mBitmapToRecycle = null;
521 }
522 });
Adrian Roos1f375ab2014-04-28 18:26:38 +0200523 }
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700524 }
525 }
526
527 public class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap> {
Sunny Goyal316490e2015-06-02 09:38:28 -0700528 @Thunk final WidgetCacheKey mKey;
Sunny Goyal4ddc4012016-03-10 12:02:29 -0800529 private final WidgetItem mInfo;
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700530 private final int mPreviewHeight;
531 private final int mPreviewWidth;
Hyunyoung Song3f471442015-04-08 19:01:34 -0700532 private final WidgetCell mCaller;
Sunny Goyal316490e2015-06-02 09:38:28 -0700533 @Thunk long[] mVersions;
534 @Thunk Bitmap mBitmapToRecycle;
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700535
Sunny Goyal4ddc4012016-03-10 12:02:29 -0800536 PreviewLoadTask(WidgetCacheKey key, WidgetItem info, int previewWidth,
Hyunyoung Song3f471442015-04-08 19:01:34 -0700537 int previewHeight, WidgetCell caller) {
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700538 mKey = key;
539 mInfo = info;
540 mPreviewHeight = previewHeight;
541 mPreviewWidth = previewWidth;
542 mCaller = caller;
Hyunyoung Song3f471442015-04-08 19:01:34 -0700543 if (DEBUG) {
544 Log.d(TAG, String.format("%s, %s, %d, %d",
545 mKey, mInfo, mPreviewHeight, mPreviewWidth));
546 }
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700547 }
548
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700549 @Override
550 protected Bitmap doInBackground(Void... params) {
551 Bitmap unusedBitmap = null;
Hyunyoung Song3f471442015-04-08 19:01:34 -0700552
Hyunyoung Songf00d02b2015-06-05 13:30:19 -0700553 // If already cancelled before this gets to run in the background, then return early
554 if (isCancelled()) {
555 return null;
556 }
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700557 synchronized (mUnusedBitmaps) {
Hyunyoung Songf00d02b2015-06-05 13:30:19 -0700558 // Check if we can re-use a bitmap
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700559 for (Bitmap candidate : mUnusedBitmaps) {
560 if (candidate != null && candidate.isMutable() &&
561 candidate.getWidth() == mPreviewWidth &&
562 candidate.getHeight() == mPreviewHeight) {
563 unusedBitmap = candidate;
Hyunyoung Songf00d02b2015-06-05 13:30:19 -0700564 mUnusedBitmaps.remove(unusedBitmap);
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700565 break;
566 }
567 }
Hyunyoung Songf00d02b2015-06-05 13:30:19 -0700568 }
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700569
Hyunyoung Songf00d02b2015-06-05 13:30:19 -0700570 // creating a bitmap is expensive. Do not do this inside synchronized block.
571 if (unusedBitmap == null) {
572 unusedBitmap = Bitmap.createBitmap(mPreviewWidth, mPreviewHeight, Config.ARGB_8888);
Adrian Roos1f375ab2014-04-28 18:26:38 +0200573 }
Winson Chung05304db2015-05-18 16:53:20 -0700574 // If cancelled now, don't bother reading the preview from the DB
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700575 if (isCancelled()) {
Winson Chung05304db2015-05-18 16:53:20 -0700576 return unusedBitmap;
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700577 }
Winson Chung05304db2015-05-18 16:53:20 -0700578 Bitmap preview = readFromDb(mKey, unusedBitmap, this);
579 // Only consider generating the preview if we have not cancelled the task already
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700580 if (!isCancelled() && preview == null) {
581 // Fetch the version info before we generate the preview, so that, in-case the
582 // app was updated while we are generating the preview, we use the old version info,
583 // which would gets re-written next time.
Winson Chung05304db2015-05-18 16:53:20 -0700584 mVersions = getPackageVersion(mKey.componentName.getPackageName());
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700585
Adam Cohen2e6da152015-05-06 11:42:25 -0700586 Launcher launcher = (Launcher) mCaller.getContext();
587
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700588 // it's not in the db... we need to generate it
Adam Cohen2e6da152015-05-06 11:42:25 -0700589 preview = generatePreview(launcher, mInfo, unusedBitmap, mPreviewWidth, mPreviewHeight);
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700590 }
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700591 return preview;
592 }
593
594 @Override
Winson Chung05304db2015-05-18 16:53:20 -0700595 protected void onPostExecute(final Bitmap preview) {
596 mCaller.applyPreview(preview);
597
598 // Write the generated preview to the DB in the worker thread
599 if (mVersions != null) {
600 mWorkerHandler.post(new Runnable() {
601 @Override
602 public void run() {
603 if (!isCancelled()) {
604 // If we are still using this preview, then write it to the DB and then
605 // let the normal clear mechanism recycle the bitmap
606 writeToDb(mKey, mVersions, preview);
607 mBitmapToRecycle = preview;
608 } else {
609 // If we've already cancelled, then skip writing the bitmap to the DB
610 // and manually add the bitmap back to the recycled set
611 synchronized (mUnusedBitmaps) {
612 mUnusedBitmaps.add(preview);
613 }
614 }
615 }
616 });
617 } else {
618 // If we don't need to write to disk, then ensure the preview gets recycled by
619 // the normal clear mechanism
620 mBitmapToRecycle = preview;
621 }
622 }
623
624 @Override
Hyunyoung Songe98f4a42015-06-16 10:45:24 -0700625 protected void onCancelled(final Bitmap preview) {
Winson Chung05304db2015-05-18 16:53:20 -0700626 // If we've cancelled while the task is running, then can return the bitmap to the
627 // recycled set immediately. Otherwise, it will be recycled after the preview is written
628 // to disk.
629 if (preview != null) {
Hyunyoung Songe98f4a42015-06-16 10:45:24 -0700630 mWorkerHandler.post(new Runnable() {
631 @Override
632 public void run() {
633 synchronized (mUnusedBitmaps) {
634 mUnusedBitmaps.add(preview);
635 }
636 }
637 });
Winson Chung05304db2015-05-18 16:53:20 -0700638 }
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700639 }
640 }
641
642 private static final class WidgetCacheKey extends ComponentKey {
643
644 // TODO: remove dependency on size
Sunny Goyal316490e2015-06-02 09:38:28 -0700645 @Thunk final String size;
Sunny Goyal5b0e6692015-03-19 14:31:19 -0700646
647 public WidgetCacheKey(ComponentName componentName, UserHandleCompat user, String size) {
648 super(componentName, user);
649 this.size = size;
650 }
651
652 @Override
653 public int hashCode() {
654 return super.hashCode() ^ size.hashCode();
655 }
656
657 @Override
658 public boolean equals(Object o) {
659 return super.equals(o) && ((WidgetCacheKey) o).size.equals(size);
Adrian Roos1f375ab2014-04-28 18:26:38 +0200660 }
661 }
Michael Jurka05713af2013-01-23 12:39:24 +0100662}