blob: 3d57acca472381583ec392ea0d320ea0cd5c15da [file] [log] [blame]
Daniel Sandler325dc232013-06-05 22:57:57 -04001package com.android.launcher3;
Michael Jurka05713af2013-01-23 12:39:24 +01002
3import android.appwidget.AppWidgetProviderInfo;
4import android.content.ComponentName;
5import android.content.ContentValues;
6import android.content.Context;
Michael Jurka8ff02ca2013-11-01 14:19:27 +01007import android.content.SharedPreferences;
Michael Jurka05713af2013-01-23 12:39:24 +01008import android.content.pm.ResolveInfo;
9import android.content.res.Resources;
10import android.database.Cursor;
Adrian Roos1f375ab2014-04-28 18:26:38 +020011import android.database.sqlite.SQLiteCantOpenDatabaseException;
Michael Jurka05713af2013-01-23 12:39:24 +010012import android.database.sqlite.SQLiteDatabase;
Michael Jurka6e27f642013-12-10 13:40:30 +010013import android.database.sqlite.SQLiteDiskIOException;
Michael Jurka05713af2013-01-23 12:39:24 +010014import android.database.sqlite.SQLiteOpenHelper;
Winson Chung5f059132014-12-01 15:05:17 -080015import android.database.sqlite.SQLiteReadOnlyDatabaseException;
Michael Jurka05713af2013-01-23 12:39:24 +010016import android.graphics.Bitmap;
17import android.graphics.Bitmap.Config;
18import android.graphics.BitmapFactory;
19import android.graphics.Canvas;
20import android.graphics.ColorMatrix;
21import android.graphics.ColorMatrixColorFilter;
22import android.graphics.Paint;
23import android.graphics.PorterDuff;
24import android.graphics.Rect;
Sunny Goyal4cad7532015-03-18 15:56:30 -070025import android.graphics.RectF;
Michael Jurka05713af2013-01-23 12:39:24 +010026import android.graphics.drawable.BitmapDrawable;
27import android.graphics.drawable.Drawable;
28import android.os.AsyncTask;
Winson Chung5f059132014-12-01 15:05:17 -080029import android.os.Build;
Michael Jurka05713af2013-01-23 12:39:24 +010030import android.util.Log;
Sunny Goyal4cad7532015-03-18 15:56:30 -070031
Sunny Goyalffe83f12014-08-14 17:39:34 -070032import com.android.launcher3.compat.AppWidgetManagerCompat;
33
Michael Jurka05713af2013-01-23 12:39:24 +010034import java.io.ByteArrayOutputStream;
35import java.io.File;
Adrian Roos1f375ab2014-04-28 18:26:38 +020036import java.io.IOException;
Michael Jurka05713af2013-01-23 12:39:24 +010037import java.lang.ref.SoftReference;
38import java.lang.ref.WeakReference;
39import java.util.ArrayList;
Adrian Roos1f375ab2014-04-28 18:26:38 +020040import java.util.Arrays;
Michael Jurka05713af2013-01-23 12:39:24 +010041import java.util.HashMap;
42import java.util.HashSet;
Adrian Roos1f375ab2014-04-28 18:26:38 +020043import java.util.List;
Adrian Roos65d60e22014-04-15 21:07:49 +020044import java.util.concurrent.Callable;
45import java.util.concurrent.ExecutionException;
Michael Jurka05713af2013-01-23 12:39:24 +010046
Sunny Goyalffe83f12014-08-14 17:39:34 -070047public class WidgetPreviewLoader {
Michael Jurka05713af2013-01-23 12:39:24 +010048
Sunny Goyalffe83f12014-08-14 17:39:34 -070049 private static final String TAG = "WidgetPreviewLoader";
50 private static final String ANDROID_INCREMENTAL_VERSION_NAME_KEY = "android.incremental.version";
51
52 private static final float WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE = 0.25f;
53 private static final HashSet<String> sInvalidPackages = new HashSet<String>();
54
Winson Chungb745afb2015-03-02 11:51:23 -080055 private final HashMap<String, WeakReference<Bitmap>> mLoadedPreviews = new HashMap<>();
56 private final ArrayList<SoftReference<Bitmap>> mUnusedBitmaps = new ArrayList<>();
Sunny Goyalffe83f12014-08-14 17:39:34 -070057
Sunny Goyalffe83f12014-08-14 17:39:34 -070058 private final int mAppIconSize;
Sunny Goyal4cad7532015-03-18 15:56:30 -070059 private final int mCellWidth;
60
61 private final Context mContext;
Sunny Goyalffe83f12014-08-14 17:39:34 -070062 private final IconCache mIconCache;
63 private final AppWidgetManagerCompat mManager;
Michael Jurka05713af2013-01-23 12:39:24 +010064
Michael Jurka3f4e0702013-02-05 11:21:28 +010065 private int mPreviewBitmapWidth;
66 private int mPreviewBitmapHeight;
Michael Jurka05713af2013-01-23 12:39:24 +010067 private String mSize;
Michael Jurka05713af2013-01-23 12:39:24 +010068
Michael Jurka05713af2013-01-23 12:39:24 +010069 private String mCachedSelectQuery;
Michael Jurkad9cb4a12013-03-19 12:01:06 +010070 private CacheDb mDb;
Michael Jurka05713af2013-01-23 12:39:24 +010071
Adrian Roos65d60e22014-04-15 21:07:49 +020072 private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
73
Chris Wrenfd13c712013-09-27 15:45:19 -040074 public WidgetPreviewLoader(Context context) {
Winson Chung5f8afe62013-08-12 16:19:28 -070075 LauncherAppState app = LauncherAppState.getInstance();
76 DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
77
Chris Wrenfd13c712013-09-27 15:45:19 -040078 mContext = context;
Winson Chung5f8afe62013-08-12 16:19:28 -070079 mAppIconSize = grid.iconSizePx;
Sunny Goyal4cad7532015-03-18 15:56:30 -070080 mCellWidth = grid.cellWidthPx;
81
Michael Jurkad9cb4a12013-03-19 12:01:06 +010082 mIconCache = app.getIconCache();
Sunny Goyalffe83f12014-08-14 17:39:34 -070083 mManager = AppWidgetManagerCompat.getInstance(context);
Michael Jurkad9cb4a12013-03-19 12:01:06 +010084 mDb = app.getWidgetPreviewCacheDb();
Michael Jurka8ff02ca2013-11-01 14:19:27 +010085
86 SharedPreferences sp = context.getSharedPreferences(
87 LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE);
88 final String lastVersionName = sp.getString(ANDROID_INCREMENTAL_VERSION_NAME_KEY, null);
89 final String versionName = android.os.Build.VERSION.INCREMENTAL;
Adam Cohen02509452014-12-04 10:34:57 -080090 final boolean isLollipopOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
Michael Jurka8ff02ca2013-11-01 14:19:27 +010091 if (!versionName.equals(lastVersionName)) {
Winson Chung5f059132014-12-01 15:05:17 -080092 try {
93 // clear all the previews whenever the system version changes, to ensure that
94 // previews are up-to-date for any apps that might have been updated with the system
95 clearDb();
96 } catch (SQLiteReadOnlyDatabaseException e) {
Adam Cohen02509452014-12-04 10:34:57 -080097 if (isLollipopOrGreater) {
Winson Chung5f059132014-12-01 15:05:17 -080098 // Workaround for Bug. 18554839, if we fail to clear the db due to the read-only
99 // issue, then ignore this error and leave the old previews
100 } else {
101 throw e;
102 }
Winson Chung5f059132014-12-01 15:05:17 -0800103 } finally {
104 SharedPreferences.Editor editor = sp.edit();
105 editor.putString(ANDROID_INCREMENTAL_VERSION_NAME_KEY, versionName);
106 editor.commit();
107 }
Michael Jurka8ff02ca2013-11-01 14:19:27 +0100108 }
Michael Jurka3f4e0702013-02-05 11:21:28 +0100109 }
Sunny Goyalffe83f12014-08-14 17:39:34 -0700110
Michael Jurka6e27f642013-12-10 13:40:30 +0100111 public void recreateDb() {
112 LauncherAppState app = LauncherAppState.getInstance();
113 app.recreateWidgetPreviewDb();
114 mDb = app.getWidgetPreviewCacheDb();
115 }
Michael Jurka3f4e0702013-02-05 11:21:28 +0100116
Sunny Goyal4cad7532015-03-18 15:56:30 -0700117 public void setPreviewSize(int previewWidth, int previewHeight) {
Michael Jurka3f4e0702013-02-05 11:21:28 +0100118 mPreviewBitmapWidth = previewWidth;
119 mPreviewBitmapHeight = previewHeight;
120 mSize = previewWidth + "x" + previewHeight;
Michael Jurka05713af2013-01-23 12:39:24 +0100121 }
122
123 public Bitmap getPreview(final Object o) {
Michael Jurkaeb1bb922013-09-26 11:29:01 -0700124 final String name = getObjectName(o);
125 final String packageName = getObjectPackage(o);
Michael Jurka05713af2013-01-23 12:39:24 +0100126 // check if the package is valid
Michael Jurka05713af2013-01-23 12:39:24 +0100127 synchronized(sInvalidPackages) {
Adrian Roos5d2704f2014-03-18 23:09:12 +0100128 boolean packageValid = !sInvalidPackages.contains(packageName);
129 if (!packageValid) {
130 return null;
131 }
Michael Jurka05713af2013-01-23 12:39:24 +0100132 }
Adrian Roos5d2704f2014-03-18 23:09:12 +0100133 synchronized(mLoadedPreviews) {
134 // check if it exists in our existing cache
135 if (mLoadedPreviews.containsKey(name)) {
136 WeakReference<Bitmap> bitmapReference = mLoadedPreviews.get(name);
137 Bitmap bitmap = bitmapReference.get();
138 if (bitmap != null) {
139 return bitmap;
Michael Jurka05713af2013-01-23 12:39:24 +0100140 }
141 }
142 }
143
144 Bitmap unusedBitmap = null;
Michael Jurka3f4e0702013-02-05 11:21:28 +0100145 synchronized(mUnusedBitmaps) {
Michael Jurka05713af2013-01-23 12:39:24 +0100146 // not in cache; we need to load it from the db
Adrian Roos5d2704f2014-03-18 23:09:12 +0100147 while (unusedBitmap == null && mUnusedBitmaps.size() > 0) {
148 Bitmap candidate = mUnusedBitmaps.remove(0).get();
149 if (candidate != null && candidate.isMutable() &&
150 candidate.getWidth() == mPreviewBitmapWidth &&
151 candidate.getHeight() == mPreviewBitmapHeight) {
152 unusedBitmap = candidate;
153 }
Michael Jurka05713af2013-01-23 12:39:24 +0100154 }
Michael Jurka05713af2013-01-23 12:39:24 +0100155 }
156
157 if (unusedBitmap == null) {
Michael Jurka3f4e0702013-02-05 11:21:28 +0100158 unusedBitmap = Bitmap.createBitmap(mPreviewBitmapWidth, mPreviewBitmapHeight,
Michael Jurka05713af2013-01-23 12:39:24 +0100159 Bitmap.Config.ARGB_8888);
160 }
Adrian Roos5d2704f2014-03-18 23:09:12 +0100161 Bitmap preview = readFromDb(name, unusedBitmap);
Michael Jurka05713af2013-01-23 12:39:24 +0100162
163 if (preview != null) {
Michael Jurka3f4e0702013-02-05 11:21:28 +0100164 synchronized(mLoadedPreviews) {
165 mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview));
Michael Jurka05713af2013-01-23 12:39:24 +0100166 }
167 return preview;
168 } else {
169 // it's not in the db... we need to generate it
170 final Bitmap generatedPreview = generatePreview(o, unusedBitmap);
171 preview = generatedPreview;
172 if (preview != unusedBitmap) {
173 throw new RuntimeException("generatePreview is not recycling the bitmap " + o);
174 }
175
Michael Jurka3f4e0702013-02-05 11:21:28 +0100176 synchronized(mLoadedPreviews) {
177 mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview));
Michael Jurka05713af2013-01-23 12:39:24 +0100178 }
179
180 // write to db on a thread pool... this can be done lazily and improves the performance
181 // of the first time widget previews are loaded
182 new AsyncTask<Void, Void, Void>() {
183 public Void doInBackground(Void ... args) {
184 writeToDb(o, generatedPreview);
185 return null;
186 }
187 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
188
189 return preview;
190 }
191 }
192
Michael Jurkaee8e99f2013-02-07 13:27:06 +0100193 public void recycleBitmap(Object o, Bitmap bitmapToRecycle) {
Michael Jurka05713af2013-01-23 12:39:24 +0100194 String name = getObjectName(o);
Michael Jurka5140cfa2013-02-15 14:50:15 +0100195 synchronized (mLoadedPreviews) {
196 if (mLoadedPreviews.containsKey(name)) {
Michael Jurka3f4e0702013-02-05 11:21:28 +0100197 Bitmap b = mLoadedPreviews.get(name).get();
Michael Jurkaee8e99f2013-02-07 13:27:06 +0100198 if (b == bitmapToRecycle) {
Michael Jurka3f4e0702013-02-05 11:21:28 +0100199 mLoadedPreviews.remove(name);
Michael Jurkaee8e99f2013-02-07 13:27:06 +0100200 if (bitmapToRecycle.isMutable()) {
Michael Jurka5140cfa2013-02-15 14:50:15 +0100201 synchronized (mUnusedBitmaps) {
202 mUnusedBitmaps.add(new SoftReference<Bitmap>(b));
203 }
Michael Jurka05713af2013-01-23 12:39:24 +0100204 }
205 } else {
206 throw new RuntimeException("Bitmap passed in doesn't match up");
207 }
208 }
209 }
210 }
211
Michael Jurkad9cb4a12013-03-19 12:01:06 +0100212 static class CacheDb extends SQLiteOpenHelper {
Michael Jurkae5919c52013-03-06 17:30:10 +0100213 final static int DB_VERSION = 2;
Michael Jurka05713af2013-01-23 12:39:24 +0100214 final static String TABLE_NAME = "shortcut_and_widget_previews";
215 final static String COLUMN_NAME = "name";
216 final static String COLUMN_SIZE = "size";
217 final static String COLUMN_PREVIEW_BITMAP = "preview_bitmap";
218 Context mContext;
219
Michael Jurkad9cb4a12013-03-19 12:01:06 +0100220 public CacheDb(Context context) {
Helena Josol4fbbb3e2014-10-06 16:06:46 +0100221 super(context, new File(context.getCacheDir(),
222 LauncherFiles.WIDGET_PREVIEWS_DB).getPath(), null, DB_VERSION);
Michael Jurka05713af2013-01-23 12:39:24 +0100223 // Store the context for later use
224 mContext = context;
225 }
226
227 @Override
228 public void onCreate(SQLiteDatabase database) {
Michael Jurka32b7a092013-02-07 20:06:49 +0100229 database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
Michael Jurka05713af2013-01-23 12:39:24 +0100230 COLUMN_NAME + " TEXT NOT NULL, " +
231 COLUMN_SIZE + " TEXT NOT NULL, " +
232 COLUMN_PREVIEW_BITMAP + " BLOB NOT NULL, " +
233 "PRIMARY KEY (" + COLUMN_NAME + ", " + COLUMN_SIZE + ") " +
Michael Jurka32b7a092013-02-07 20:06:49 +0100234 ");");
Michael Jurka05713af2013-01-23 12:39:24 +0100235 }
236
237 @Override
238 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Michael Jurkae5919c52013-03-06 17:30:10 +0100239 if (oldVersion != newVersion) {
240 // Delete all the records; they'll be repopulated as this is a cache
241 db.execSQL("DELETE FROM " + TABLE_NAME);
242 }
Michael Jurka05713af2013-01-23 12:39:24 +0100243 }
244 }
245
246 private static final String WIDGET_PREFIX = "Widget:";
247 private static final String SHORTCUT_PREFIX = "Shortcut:";
248
249 private static String getObjectName(Object o) {
250 // should cache the string builder
251 StringBuilder sb = new StringBuilder();
252 String output;
253 if (o instanceof AppWidgetProviderInfo) {
254 sb.append(WIDGET_PREFIX);
Sunny Goyalffe83f12014-08-14 17:39:34 -0700255 sb.append(((AppWidgetProviderInfo) o).toString());
Michael Jurka05713af2013-01-23 12:39:24 +0100256 output = sb.toString();
257 sb.setLength(0);
258 } else {
259 sb.append(SHORTCUT_PREFIX);
Michael Jurka05713af2013-01-23 12:39:24 +0100260 ResolveInfo info = (ResolveInfo) o;
261 sb.append(new ComponentName(info.activityInfo.packageName,
262 info.activityInfo.name).flattenToString());
263 output = sb.toString();
264 sb.setLength(0);
265 }
266 return output;
267 }
268
269 private String getObjectPackage(Object o) {
270 if (o instanceof AppWidgetProviderInfo) {
271 return ((AppWidgetProviderInfo) o).provider.getPackageName();
272 } else {
273 ResolveInfo info = (ResolveInfo) o;
274 return info.activityInfo.packageName;
275 }
276 }
277
278 private void writeToDb(Object o, Bitmap preview) {
279 String name = getObjectName(o);
280 SQLiteDatabase db = mDb.getWritableDatabase();
281 ContentValues values = new ContentValues();
282
Michael Jurkad9cb4a12013-03-19 12:01:06 +0100283 values.put(CacheDb.COLUMN_NAME, name);
Michael Jurka05713af2013-01-23 12:39:24 +0100284 ByteArrayOutputStream stream = new ByteArrayOutputStream();
285 preview.compress(Bitmap.CompressFormat.PNG, 100, stream);
Michael Jurkad9cb4a12013-03-19 12:01:06 +0100286 values.put(CacheDb.COLUMN_PREVIEW_BITMAP, stream.toByteArray());
287 values.put(CacheDb.COLUMN_SIZE, mSize);
Michael Jurka6e27f642013-12-10 13:40:30 +0100288 try {
289 db.insert(CacheDb.TABLE_NAME, null, values);
290 } catch (SQLiteDiskIOException e) {
291 recreateDb();
Adrian Roos1f375ab2014-04-28 18:26:38 +0200292 } catch (SQLiteCantOpenDatabaseException e) {
293 dumpOpenFiles();
294 throw e;
Michael Jurka6e27f642013-12-10 13:40:30 +0100295 }
Michael Jurka05713af2013-01-23 12:39:24 +0100296 }
297
Michael Jurka8ff02ca2013-11-01 14:19:27 +0100298 private void clearDb() {
299 SQLiteDatabase db = mDb.getWritableDatabase();
300 // Delete everything
Michael Jurka6e27f642013-12-10 13:40:30 +0100301 try {
302 db.delete(CacheDb.TABLE_NAME, null, null);
303 } catch (SQLiteDiskIOException e) {
Adrian Roos1f375ab2014-04-28 18:26:38 +0200304 } catch (SQLiteCantOpenDatabaseException e) {
305 dumpOpenFiles();
306 throw e;
Michael Jurka6e27f642013-12-10 13:40:30 +0100307 }
Michael Jurka8ff02ca2013-11-01 14:19:27 +0100308 }
309
Michael Jurkaeb1bb922013-09-26 11:29:01 -0700310 public static void removePackageFromDb(final CacheDb cacheDb, final String packageName) {
Michael Jurka05713af2013-01-23 12:39:24 +0100311 synchronized(sInvalidPackages) {
312 sInvalidPackages.add(packageName);
313 }
314 new AsyncTask<Void, Void, Void>() {
315 public Void doInBackground(Void ... args) {
Michael Jurkad9cb4a12013-03-19 12:01:06 +0100316 SQLiteDatabase db = cacheDb.getWritableDatabase();
Michael Jurka6e27f642013-12-10 13:40:30 +0100317 try {
318 db.delete(CacheDb.TABLE_NAME,
319 CacheDb.COLUMN_NAME + " LIKE ? OR " +
320 CacheDb.COLUMN_NAME + " LIKE ?", // SELECT query
321 new String[] {
322 WIDGET_PREFIX + packageName + "/%",
323 SHORTCUT_PREFIX + packageName + "/%"
324 } // args to SELECT query
325 );
326 } catch (SQLiteDiskIOException e) {
Adrian Roos1f375ab2014-04-28 18:26:38 +0200327 } catch (SQLiteCantOpenDatabaseException e) {
328 dumpOpenFiles();
329 throw e;
Michael Jurka6e27f642013-12-10 13:40:30 +0100330 }
Michael Jurka05713af2013-01-23 12:39:24 +0100331 synchronized(sInvalidPackages) {
332 sInvalidPackages.remove(packageName);
333 }
334 return null;
335 }
336 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
337 }
338
Sunny Goyalffe83f12014-08-14 17:39:34 -0700339 private static void removeItemFromDb(final CacheDb cacheDb, final String objectName) {
Michael Jurkaeb1bb922013-09-26 11:29:01 -0700340 new AsyncTask<Void, Void, Void>() {
341 public Void doInBackground(Void ... args) {
342 SQLiteDatabase db = cacheDb.getWritableDatabase();
Michael Jurka6e27f642013-12-10 13:40:30 +0100343 try {
344 db.delete(CacheDb.TABLE_NAME,
345 CacheDb.COLUMN_NAME + " = ? ", // SELECT query
346 new String[] { objectName }); // args to SELECT query
347 } catch (SQLiteDiskIOException e) {
Adrian Roos1f375ab2014-04-28 18:26:38 +0200348 } catch (SQLiteCantOpenDatabaseException e) {
349 dumpOpenFiles();
350 throw e;
Michael Jurka6e27f642013-12-10 13:40:30 +0100351 }
Michael Jurkaeb1bb922013-09-26 11:29:01 -0700352 return null;
353 }
354 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
355 }
356
Michael Jurka05713af2013-01-23 12:39:24 +0100357 private Bitmap readFromDb(String name, Bitmap b) {
358 if (mCachedSelectQuery == null) {
Michael Jurkad9cb4a12013-03-19 12:01:06 +0100359 mCachedSelectQuery = CacheDb.COLUMN_NAME + " = ? AND " +
360 CacheDb.COLUMN_SIZE + " = ?";
Michael Jurka05713af2013-01-23 12:39:24 +0100361 }
362 SQLiteDatabase db = mDb.getReadableDatabase();
Michael Jurka6e27f642013-12-10 13:40:30 +0100363 Cursor result;
364 try {
365 result = db.query(CacheDb.TABLE_NAME,
366 new String[] { CacheDb.COLUMN_PREVIEW_BITMAP }, // cols to return
367 mCachedSelectQuery, // select query
368 new String[] { name, mSize }, // args to select query
369 null,
370 null,
371 null,
372 null);
373 } catch (SQLiteDiskIOException e) {
374 recreateDb();
375 return null;
Adrian Roos1f375ab2014-04-28 18:26:38 +0200376 } catch (SQLiteCantOpenDatabaseException e) {
377 dumpOpenFiles();
378 throw e;
Michael Jurka6e27f642013-12-10 13:40:30 +0100379 }
Michael Jurka05713af2013-01-23 12:39:24 +0100380 if (result.getCount() > 0) {
381 result.moveToFirst();
382 byte[] blob = result.getBlob(0);
383 result.close();
Sunny Goyal4cad7532015-03-18 15:56:30 -0700384 final BitmapFactory.Options opts = new BitmapFactory.Options();
Michael Jurka05713af2013-01-23 12:39:24 +0100385 opts.inBitmap = b;
386 opts.inSampleSize = 1;
Michael Jurkaeb1bb922013-09-26 11:29:01 -0700387 try {
388 return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts);
389 } catch (IllegalArgumentException e) {
390 removeItemFromDb(mDb, name);
391 return null;
392 }
Michael Jurka05713af2013-01-23 12:39:24 +0100393 } else {
394 result.close();
395 return null;
396 }
397 }
398
Sunny Goyalffe83f12014-08-14 17:39:34 -0700399 private Bitmap generatePreview(Object info, Bitmap preview) {
Michael Jurka05713af2013-01-23 12:39:24 +0100400 if (preview != null &&
Michael Jurka3f4e0702013-02-05 11:21:28 +0100401 (preview.getWidth() != mPreviewBitmapWidth ||
402 preview.getHeight() != mPreviewBitmapHeight)) {
Michael Jurka05713af2013-01-23 12:39:24 +0100403 throw new RuntimeException("Improperly sized bitmap passed as argument");
404 }
Adam Cohen59400422014-03-05 18:07:04 -0800405 if (info instanceof LauncherAppWidgetProviderInfo) {
406 return generateWidgetPreview((LauncherAppWidgetProviderInfo) info, preview);
Michael Jurka05713af2013-01-23 12:39:24 +0100407 } else {
408 return generateShortcutPreview(
Michael Jurka3f4e0702013-02-05 11:21:28 +0100409 (ResolveInfo) info, mPreviewBitmapWidth, mPreviewBitmapHeight, preview);
Michael Jurka05713af2013-01-23 12:39:24 +0100410 }
411 }
412
Adam Cohen59400422014-03-05 18:07:04 -0800413 public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info, Bitmap preview) {
414 int maxWidth = maxWidthForWidgetPreview(info.spanX);
Sunny Goyal4cad7532015-03-18 15:56:30 -0700415 return generateWidgetPreview(info, maxWidth, preview, null);
Michael Jurka05713af2013-01-23 12:39:24 +0100416 }
417
418 public int maxWidthForWidgetPreview(int spanX) {
Sunny Goyal4cad7532015-03-18 15:56:30 -0700419 return Math.min(mPreviewBitmapWidth, spanX * mCellWidth);
Michael Jurka05713af2013-01-23 12:39:24 +0100420 }
421
Sunny Goyal4cad7532015-03-18 15:56:30 -0700422 public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info,
423 int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) {
Michael Jurka05713af2013-01-23 12:39:24 +0100424 // Load the preview image if possible
Michael Jurka05713af2013-01-23 12:39:24 +0100425 if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE;
Michael Jurka05713af2013-01-23 12:39:24 +0100426
427 Drawable drawable = null;
Sunny Goyalffe83f12014-08-14 17:39:34 -0700428 if (info.previewImage != 0) {
429 drawable = mManager.loadPreview(info);
Adrian Roosfa9ffc22014-05-12 15:59:59 +0200430 if (drawable != null) {
431 drawable = mutateOnMainThread(drawable);
432 } else {
Michael Jurka05713af2013-01-23 12:39:24 +0100433 Log.w(TAG, "Can't load widget preview drawable 0x" +
Sunny Goyalffe83f12014-08-14 17:39:34 -0700434 Integer.toHexString(info.previewImage) + " for provider: " + info.provider);
Michael Jurka05713af2013-01-23 12:39:24 +0100435 }
436 }
437
Sunny Goyal4cad7532015-03-18 15:56:30 -0700438 final boolean widgetPreviewExists = (drawable != null);
439 final int spanX = info.spanX < 1 ? 1 : info.spanX;
440 final int spanY = info.spanY < 1 ? 1 : info.spanY;
441
Michael Jurka05713af2013-01-23 12:39:24 +0100442 int previewWidth;
443 int previewHeight;
Sunny Goyal4cad7532015-03-18 15:56:30 -0700444 Bitmap tileBitmap = null;
445
Michael Jurka05713af2013-01-23 12:39:24 +0100446 if (widgetPreviewExists) {
447 previewWidth = drawable.getIntrinsicWidth();
448 previewHeight = drawable.getIntrinsicHeight();
449 } else {
450 // Generate a preview image if we couldn't load one
Sunny Goyal4cad7532015-03-18 15:56:30 -0700451 tileBitmap = ((BitmapDrawable) mContext.getResources().getDrawable(
452 R.drawable.widget_tile)).getBitmap();
453 previewWidth = tileBitmap.getWidth() * spanX;
454 previewHeight = tileBitmap.getHeight() * spanY;
Michael Jurka05713af2013-01-23 12:39:24 +0100455 }
456
457 // Scale to fit width only - let the widget preview be clipped in the
458 // vertical dimension
459 float scale = 1f;
460 if (preScaledWidthOut != null) {
461 preScaledWidthOut[0] = previewWidth;
462 }
463 if (previewWidth > maxPreviewWidth) {
464 scale = maxPreviewWidth / (float) previewWidth;
465 }
466 if (scale != 1f) {
467 previewWidth = (int) (scale * previewWidth);
468 previewHeight = (int) (scale * previewHeight);
469 }
470
471 // 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 -0700472 final Canvas c = new Canvas();
Michael Jurka05713af2013-01-23 12:39:24 +0100473 if (preview == null) {
474 preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);
Sunny Goyal4cad7532015-03-18 15:56:30 -0700475 c.setBitmap(preview);
476 } else {
477 // Reusing bitmap. Clear it.
478 c.setBitmap(preview);
479 c.drawColor(0, PorterDuff.Mode.CLEAR);
Michael Jurka05713af2013-01-23 12:39:24 +0100480 }
481
482 // Draw the scaled preview into the final bitmap
483 int x = (preview.getWidth() - previewWidth) / 2;
484 if (widgetPreviewExists) {
Sunny Goyal4cad7532015-03-18 15:56:30 -0700485 drawable.setBounds(x, 0, x + previewWidth, previewHeight);
486 drawable.draw(c);
Michael Jurka05713af2013-01-23 12:39:24 +0100487 } else {
Sunny Goyal4cad7532015-03-18 15:56:30 -0700488 final Paint p = new Paint();
489 p.setFilterBitmap(true);
Michael Jurka05713af2013-01-23 12:39:24 +0100490
Sunny Goyal4cad7532015-03-18 15:56:30 -0700491 // draw the spanX x spanY tiles
492 final Rect src = new Rect(0, 0, tileBitmap.getWidth(), tileBitmap.getHeight());
493
494 float tileW = scale * tileBitmap.getWidth();
495 float tileH = scale * tileBitmap.getHeight();
496 final RectF dst = new RectF(0, 0, tileW, tileH);
497
498 float tx = x;
499 for (int i = 0; i < spanX; i++, tx += tileW) {
500 float ty = 0;
501 for (int j = 0; j < spanY; j++, ty += tileH) {
502 dst.offsetTo(tx, ty);
503 c.drawBitmap(tileBitmap, src, dst, p);
504 }
Michael Jurka05713af2013-01-23 12:39:24 +0100505 }
Sunny Goyal4cad7532015-03-18 15:56:30 -0700506
507 // Draw the icon in the top left corner
508 // TODO: use top right for RTL
509 int minOffset = (int) (mAppIconSize * WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE);
510 int smallestSide = Math.min(previewWidth, previewHeight);
511 float iconScale = Math.min((float) smallestSide / (mAppIconSize + 2 * minOffset), scale);
512
513 try {
514 Drawable icon = mutateOnMainThread(mManager.loadIcon(info, mIconCache));
515 if (icon != null) {
516 int hoffset = (int) ((tileW - mAppIconSize * iconScale) / 2) + x;
517 int yoffset = (int) ((tileH - mAppIconSize * iconScale) / 2);
518 icon.setBounds(hoffset, yoffset,
519 hoffset + (int) (mAppIconSize * iconScale),
520 yoffset + (int) (mAppIconSize * iconScale));
521 icon.draw(c);
522 }
523 } catch (Resources.NotFoundException e) { }
Michael Jurka05713af2013-01-23 12:39:24 +0100524 c.setBitmap(null);
525 }
Sunny Goyalffe83f12014-08-14 17:39:34 -0700526 return mManager.getBadgeBitmap(info, preview);
Michael Jurka05713af2013-01-23 12:39:24 +0100527 }
528
529 private Bitmap generateShortcutPreview(
530 ResolveInfo info, int maxWidth, int maxHeight, Bitmap preview) {
Sunny Goyal4cad7532015-03-18 15:56:30 -0700531 final Canvas c = new Canvas();
532 if (preview == null) {
Michael Jurka05713af2013-01-23 12:39:24 +0100533 preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888);
Sunny Goyal4cad7532015-03-18 15:56:30 -0700534 c.setBitmap(preview);
535 } else if (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight) {
536 throw new RuntimeException("Improperly sized bitmap passed as argument");
537 } else {
538 // Reusing bitmap. Clear it.
539 c.setBitmap(preview);
540 c.drawColor(0, PorterDuff.Mode.CLEAR);
Michael Jurka05713af2013-01-23 12:39:24 +0100541 }
542
Sunny Goyal4cad7532015-03-18 15:56:30 -0700543 Drawable icon = mutateOnMainThread(mIconCache.getFullResIcon(info.activityInfo));
544 icon.setFilterBitmap(true);
545
Michael Jurka05713af2013-01-23 12:39:24 +0100546 // Draw a desaturated/scaled version of the icon in the background as a watermark
Sunny Goyal4cad7532015-03-18 15:56:30 -0700547 ColorMatrix colorMatrix = new ColorMatrix();
548 colorMatrix.setSaturation(0);
549 icon.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
550 icon.setAlpha((int) (255 * 0.06f));
551
552 Resources res = mContext.getResources();
553 int paddingTop = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top);
554 int paddingLeft = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left);
555 int paddingRight = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right);
556 int scaledIconWidth = (maxWidth - paddingLeft - paddingRight);
557 icon.setBounds(paddingLeft, paddingTop,
558 paddingLeft + scaledIconWidth, paddingTop + scaledIconWidth);
559 icon.draw(c);
560
561 // Draw the final icon at top left corner.
562 // TODO: use top right for RTL
563 icon.setAlpha(255);
564 icon.setColorFilter(null);
565 icon.setBounds(0, 0, mAppIconSize, mAppIconSize);
566 icon.draw(c);
567
Michael Jurka05713af2013-01-23 12:39:24 +0100568 c.setBitmap(null);
Michael Jurka05713af2013-01-23 12:39:24 +0100569 return preview;
570 }
571
Adrian Roos65d60e22014-04-15 21:07:49 +0200572 private Drawable mutateOnMainThread(final Drawable drawable) {
573 try {
574 return mMainThreadExecutor.submit(new Callable<Drawable>() {
575 @Override
576 public Drawable call() throws Exception {
577 return drawable.mutate();
578 }
579 }).get();
580 } catch (InterruptedException e) {
581 Thread.currentThread().interrupt();
582 throw new RuntimeException(e);
583 } catch (ExecutionException e) {
584 throw new RuntimeException(e);
585 }
586 }
Adrian Roos1f375ab2014-04-28 18:26:38 +0200587
588 private static final int MAX_OPEN_FILES = 1024;
589 private static final int SAMPLE_RATE = 23;
590 /**
591 * Dumps all files that are open in this process without allocating a file descriptor.
592 */
593 private static void dumpOpenFiles() {
594 try {
595 Log.i(TAG, "DUMP OF OPEN FILES (sample rate: 1 every " + SAMPLE_RATE + "):");
596 final String TYPE_APK = "apk";
597 final String TYPE_JAR = "jar";
598 final String TYPE_PIPE = "pipe";
599 final String TYPE_SOCKET = "socket";
600 final String TYPE_DB = "db";
601 final String TYPE_ANON_INODE = "anon_inode";
602 final String TYPE_DEV = "dev";
603 final String TYPE_NON_FS = "non-fs";
604 final String TYPE_OTHER = "other";
605 List<String> types = Arrays.asList(TYPE_APK, TYPE_JAR, TYPE_PIPE, TYPE_SOCKET, TYPE_DB,
606 TYPE_ANON_INODE, TYPE_DEV, TYPE_NON_FS, TYPE_OTHER);
607 int[] count = new int[types.size()];
608 int[] duplicates = new int[types.size()];
609 HashSet<String> files = new HashSet<String>();
610 int total = 0;
611 for (int i = 0; i < MAX_OPEN_FILES; i++) {
612 // This is a gigantic hack but unfortunately the only way to resolve an fd
613 // to a file name. Note that we have to loop over all possible fds because
614 // reading the directory would require allocating a new fd. The kernel is
615 // currently implemented such that no fd is larger then the current rlimit,
616 // which is why it's safe to loop over them in such a way.
617 String fd = "/proc/self/fd/" + i;
618 try {
619 // getCanonicalPath() uses readlink behind the scene which doesn't require
620 // a file descriptor.
621 String resolved = new File(fd).getCanonicalPath();
622 int type = types.indexOf(TYPE_OTHER);
623 if (resolved.startsWith("/dev/")) {
624 type = types.indexOf(TYPE_DEV);
625 } else if (resolved.endsWith(".apk")) {
626 type = types.indexOf(TYPE_APK);
627 } else if (resolved.endsWith(".jar")) {
628 type = types.indexOf(TYPE_JAR);
629 } else if (resolved.contains("/fd/pipe:")) {
630 type = types.indexOf(TYPE_PIPE);
631 } else if (resolved.contains("/fd/socket:")) {
632 type = types.indexOf(TYPE_SOCKET);
633 } else if (resolved.contains("/fd/anon_inode:")) {
634 type = types.indexOf(TYPE_ANON_INODE);
635 } else if (resolved.endsWith(".db") || resolved.contains("/databases/")) {
636 type = types.indexOf(TYPE_DB);
637 } else if (resolved.startsWith("/proc/") && resolved.contains("/fd/")) {
638 // Those are the files that don't point anywhere on the file system.
639 // getCanonicalPath() wrongly interprets these as relative symlinks and
640 // resolves them within /proc/<pid>/fd/.
641 type = types.indexOf(TYPE_NON_FS);
642 }
643 count[type]++;
644 total++;
645 if (files.contains(resolved)) {
646 duplicates[type]++;
647 }
648 files.add(resolved);
649 if (total % SAMPLE_RATE == 0) {
650 Log.i(TAG, " fd " + i + ": " + resolved
651 + " (" + types.get(type) + ")");
652 }
653 } catch (IOException e) {
654 // Ignoring exceptions for non-existing file descriptors.
655 }
656 }
657 for (int i = 0; i < types.size(); i++) {
658 Log.i(TAG, String.format("Open %10s files: %4d total, %4d duplicates",
659 types.get(i), count[i], duplicates[i]));
660 }
661 } catch (Throwable t) {
662 // Catch everything. This is called from an exception handler that we shouldn't upset.
663 Log.e(TAG, "Unable to log open files.", t);
664 }
665 }
Michael Jurka05713af2013-01-23 12:39:24 +0100666}