blob: 0a5f0af4e91f99b200184298ad6c6b759ca57a18 [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;
Adam Cohen091440a2015-03-18 14:16:05 -070033import com.android.launcher3.util.Thunk;
Sunny Goyalffe83f12014-08-14 17:39:34 -070034
Michael Jurka05713af2013-01-23 12:39:24 +010035import java.io.ByteArrayOutputStream;
36import java.io.File;
Adrian Roos1f375ab2014-04-28 18:26:38 +020037import java.io.IOException;
Michael Jurka05713af2013-01-23 12:39:24 +010038import java.lang.ref.SoftReference;
39import java.lang.ref.WeakReference;
40import java.util.ArrayList;
Adrian Roos1f375ab2014-04-28 18:26:38 +020041import java.util.Arrays;
Michael Jurka05713af2013-01-23 12:39:24 +010042import java.util.HashMap;
43import java.util.HashSet;
Adrian Roos1f375ab2014-04-28 18:26:38 +020044import java.util.List;
Adrian Roos65d60e22014-04-15 21:07:49 +020045import java.util.concurrent.Callable;
46import java.util.concurrent.ExecutionException;
Michael Jurka05713af2013-01-23 12:39:24 +010047
Sunny Goyalffe83f12014-08-14 17:39:34 -070048public class WidgetPreviewLoader {
Michael Jurka05713af2013-01-23 12:39:24 +010049
Sunny Goyalffe83f12014-08-14 17:39:34 -070050 private static final String TAG = "WidgetPreviewLoader";
51 private static final String ANDROID_INCREMENTAL_VERSION_NAME_KEY = "android.incremental.version";
52
53 private static final float WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE = 0.25f;
Adam Cohen091440a2015-03-18 14:16:05 -070054 @Thunk static final HashSet<String> sInvalidPackages = new HashSet<String>();
Sunny Goyalffe83f12014-08-14 17:39:34 -070055
Winson Chungb745afb2015-03-02 11:51:23 -080056 private final HashMap<String, WeakReference<Bitmap>> mLoadedPreviews = new HashMap<>();
57 private final ArrayList<SoftReference<Bitmap>> mUnusedBitmaps = new ArrayList<>();
Sunny Goyalffe83f12014-08-14 17:39:34 -070058
Sunny Goyalffe83f12014-08-14 17:39:34 -070059 private final int mAppIconSize;
Sunny Goyal4cad7532015-03-18 15:56:30 -070060 private final int mCellWidth;
61
62 private final Context mContext;
Sunny Goyalffe83f12014-08-14 17:39:34 -070063 private final IconCache mIconCache;
64 private final AppWidgetManagerCompat mManager;
Michael Jurka05713af2013-01-23 12:39:24 +010065
Michael Jurka3f4e0702013-02-05 11:21:28 +010066 private int mPreviewBitmapWidth;
67 private int mPreviewBitmapHeight;
Michael Jurka05713af2013-01-23 12:39:24 +010068 private String mSize;
Michael Jurka05713af2013-01-23 12:39:24 +010069
Michael Jurka05713af2013-01-23 12:39:24 +010070 private String mCachedSelectQuery;
Michael Jurkad9cb4a12013-03-19 12:01:06 +010071 private CacheDb mDb;
Michael Jurka05713af2013-01-23 12:39:24 +010072
Adrian Roos65d60e22014-04-15 21:07:49 +020073 private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
74
Chris Wrenfd13c712013-09-27 15:45:19 -040075 public WidgetPreviewLoader(Context context) {
Winson Chung5f8afe62013-08-12 16:19:28 -070076 LauncherAppState app = LauncherAppState.getInstance();
77 DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
78
Chris Wrenfd13c712013-09-27 15:45:19 -040079 mContext = context;
Winson Chung5f8afe62013-08-12 16:19:28 -070080 mAppIconSize = grid.iconSizePx;
Sunny Goyal4cad7532015-03-18 15:56:30 -070081 mCellWidth = grid.cellWidthPx;
82
Michael Jurkad9cb4a12013-03-19 12:01:06 +010083 mIconCache = app.getIconCache();
Sunny Goyalffe83f12014-08-14 17:39:34 -070084 mManager = AppWidgetManagerCompat.getInstance(context);
Michael Jurkad9cb4a12013-03-19 12:01:06 +010085 mDb = app.getWidgetPreviewCacheDb();
Michael Jurka8ff02ca2013-11-01 14:19:27 +010086
87 SharedPreferences sp = context.getSharedPreferences(
88 LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE);
89 final String lastVersionName = sp.getString(ANDROID_INCREMENTAL_VERSION_NAME_KEY, null);
90 final String versionName = android.os.Build.VERSION.INCREMENTAL;
Adam Cohen02509452014-12-04 10:34:57 -080091 final boolean isLollipopOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
Michael Jurka8ff02ca2013-11-01 14:19:27 +010092 if (!versionName.equals(lastVersionName)) {
Winson Chung5f059132014-12-01 15:05:17 -080093 try {
94 // clear all the previews whenever the system version changes, to ensure that
95 // previews are up-to-date for any apps that might have been updated with the system
96 clearDb();
97 } catch (SQLiteReadOnlyDatabaseException e) {
Adam Cohen02509452014-12-04 10:34:57 -080098 if (isLollipopOrGreater) {
Winson Chung5f059132014-12-01 15:05:17 -080099 // Workaround for Bug. 18554839, if we fail to clear the db due to the read-only
100 // issue, then ignore this error and leave the old previews
101 } else {
102 throw e;
103 }
Winson Chung5f059132014-12-01 15:05:17 -0800104 } finally {
105 SharedPreferences.Editor editor = sp.edit();
106 editor.putString(ANDROID_INCREMENTAL_VERSION_NAME_KEY, versionName);
107 editor.commit();
108 }
Michael Jurka8ff02ca2013-11-01 14:19:27 +0100109 }
Michael Jurka3f4e0702013-02-05 11:21:28 +0100110 }
Sunny Goyalffe83f12014-08-14 17:39:34 -0700111
Michael Jurka6e27f642013-12-10 13:40:30 +0100112 public void recreateDb() {
113 LauncherAppState app = LauncherAppState.getInstance();
114 app.recreateWidgetPreviewDb();
115 mDb = app.getWidgetPreviewCacheDb();
116 }
Michael Jurka3f4e0702013-02-05 11:21:28 +0100117
Sunny Goyal4cad7532015-03-18 15:56:30 -0700118 public void setPreviewSize(int previewWidth, int previewHeight) {
Michael Jurka3f4e0702013-02-05 11:21:28 +0100119 mPreviewBitmapWidth = previewWidth;
120 mPreviewBitmapHeight = previewHeight;
121 mSize = previewWidth + "x" + previewHeight;
Michael Jurka05713af2013-01-23 12:39:24 +0100122 }
123
124 public Bitmap getPreview(final Object o) {
Michael Jurkaeb1bb922013-09-26 11:29:01 -0700125 final String name = getObjectName(o);
126 final String packageName = getObjectPackage(o);
Michael Jurka05713af2013-01-23 12:39:24 +0100127 // check if the package is valid
Michael Jurka05713af2013-01-23 12:39:24 +0100128 synchronized(sInvalidPackages) {
Adrian Roos5d2704f2014-03-18 23:09:12 +0100129 boolean packageValid = !sInvalidPackages.contains(packageName);
130 if (!packageValid) {
131 return null;
132 }
Michael Jurka05713af2013-01-23 12:39:24 +0100133 }
Adrian Roos5d2704f2014-03-18 23:09:12 +0100134 synchronized(mLoadedPreviews) {
135 // check if it exists in our existing cache
136 if (mLoadedPreviews.containsKey(name)) {
137 WeakReference<Bitmap> bitmapReference = mLoadedPreviews.get(name);
138 Bitmap bitmap = bitmapReference.get();
139 if (bitmap != null) {
140 return bitmap;
Michael Jurka05713af2013-01-23 12:39:24 +0100141 }
142 }
143 }
144
145 Bitmap unusedBitmap = null;
Michael Jurka3f4e0702013-02-05 11:21:28 +0100146 synchronized(mUnusedBitmaps) {
Michael Jurka05713af2013-01-23 12:39:24 +0100147 // not in cache; we need to load it from the db
Adrian Roos5d2704f2014-03-18 23:09:12 +0100148 while (unusedBitmap == null && mUnusedBitmaps.size() > 0) {
149 Bitmap candidate = mUnusedBitmaps.remove(0).get();
150 if (candidate != null && candidate.isMutable() &&
151 candidate.getWidth() == mPreviewBitmapWidth &&
152 candidate.getHeight() == mPreviewBitmapHeight) {
153 unusedBitmap = candidate;
154 }
Michael Jurka05713af2013-01-23 12:39:24 +0100155 }
Michael Jurka05713af2013-01-23 12:39:24 +0100156 }
157
158 if (unusedBitmap == null) {
Michael Jurka3f4e0702013-02-05 11:21:28 +0100159 unusedBitmap = Bitmap.createBitmap(mPreviewBitmapWidth, mPreviewBitmapHeight,
Michael Jurka05713af2013-01-23 12:39:24 +0100160 Bitmap.Config.ARGB_8888);
161 }
Adrian Roos5d2704f2014-03-18 23:09:12 +0100162 Bitmap preview = readFromDb(name, unusedBitmap);
Michael Jurka05713af2013-01-23 12:39:24 +0100163
164 if (preview != null) {
Michael Jurka3f4e0702013-02-05 11:21:28 +0100165 synchronized(mLoadedPreviews) {
166 mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview));
Michael Jurka05713af2013-01-23 12:39:24 +0100167 }
168 return preview;
169 } else {
170 // it's not in the db... we need to generate it
171 final Bitmap generatedPreview = generatePreview(o, unusedBitmap);
172 preview = generatedPreview;
173 if (preview != unusedBitmap) {
174 throw new RuntimeException("generatePreview is not recycling the bitmap " + o);
175 }
176
Michael Jurka3f4e0702013-02-05 11:21:28 +0100177 synchronized(mLoadedPreviews) {
178 mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview));
Michael Jurka05713af2013-01-23 12:39:24 +0100179 }
180
181 // write to db on a thread pool... this can be done lazily and improves the performance
182 // of the first time widget previews are loaded
183 new AsyncTask<Void, Void, Void>() {
184 public Void doInBackground(Void ... args) {
185 writeToDb(o, generatedPreview);
186 return null;
187 }
188 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
189
190 return preview;
191 }
192 }
193
Michael Jurkaee8e99f2013-02-07 13:27:06 +0100194 public void recycleBitmap(Object o, Bitmap bitmapToRecycle) {
Michael Jurka05713af2013-01-23 12:39:24 +0100195 String name = getObjectName(o);
Michael Jurka5140cfa2013-02-15 14:50:15 +0100196 synchronized (mLoadedPreviews) {
197 if (mLoadedPreviews.containsKey(name)) {
Michael Jurka3f4e0702013-02-05 11:21:28 +0100198 Bitmap b = mLoadedPreviews.get(name).get();
Michael Jurkaee8e99f2013-02-07 13:27:06 +0100199 if (b == bitmapToRecycle) {
Michael Jurka3f4e0702013-02-05 11:21:28 +0100200 mLoadedPreviews.remove(name);
Michael Jurkaee8e99f2013-02-07 13:27:06 +0100201 if (bitmapToRecycle.isMutable()) {
Michael Jurka5140cfa2013-02-15 14:50:15 +0100202 synchronized (mUnusedBitmaps) {
203 mUnusedBitmaps.add(new SoftReference<Bitmap>(b));
204 }
Michael Jurka05713af2013-01-23 12:39:24 +0100205 }
206 } else {
207 throw new RuntimeException("Bitmap passed in doesn't match up");
208 }
209 }
210 }
211 }
212
Michael Jurkad9cb4a12013-03-19 12:01:06 +0100213 static class CacheDb extends SQLiteOpenHelper {
Michael Jurkae5919c52013-03-06 17:30:10 +0100214 final static int DB_VERSION = 2;
Michael Jurka05713af2013-01-23 12:39:24 +0100215 final static String TABLE_NAME = "shortcut_and_widget_previews";
216 final static String COLUMN_NAME = "name";
217 final static String COLUMN_SIZE = "size";
218 final static String COLUMN_PREVIEW_BITMAP = "preview_bitmap";
219 Context mContext;
220
Michael Jurkad9cb4a12013-03-19 12:01:06 +0100221 public CacheDb(Context context) {
Helena Josol4fbbb3e2014-10-06 16:06:46 +0100222 super(context, new File(context.getCacheDir(),
223 LauncherFiles.WIDGET_PREVIEWS_DB).getPath(), null, DB_VERSION);
Michael Jurka05713af2013-01-23 12:39:24 +0100224 // Store the context for later use
225 mContext = context;
226 }
227
228 @Override
229 public void onCreate(SQLiteDatabase database) {
Michael Jurka32b7a092013-02-07 20:06:49 +0100230 database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
Michael Jurka05713af2013-01-23 12:39:24 +0100231 COLUMN_NAME + " TEXT NOT NULL, " +
232 COLUMN_SIZE + " TEXT NOT NULL, " +
233 COLUMN_PREVIEW_BITMAP + " BLOB NOT NULL, " +
234 "PRIMARY KEY (" + COLUMN_NAME + ", " + COLUMN_SIZE + ") " +
Michael Jurka32b7a092013-02-07 20:06:49 +0100235 ");");
Michael Jurka05713af2013-01-23 12:39:24 +0100236 }
237
238 @Override
239 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Michael Jurkae5919c52013-03-06 17:30:10 +0100240 if (oldVersion != newVersion) {
241 // Delete all the records; they'll be repopulated as this is a cache
242 db.execSQL("DELETE FROM " + TABLE_NAME);
243 }
Michael Jurka05713af2013-01-23 12:39:24 +0100244 }
245 }
246
247 private static final String WIDGET_PREFIX = "Widget:";
248 private static final String SHORTCUT_PREFIX = "Shortcut:";
249
250 private static String getObjectName(Object o) {
251 // should cache the string builder
252 StringBuilder sb = new StringBuilder();
253 String output;
254 if (o instanceof AppWidgetProviderInfo) {
255 sb.append(WIDGET_PREFIX);
Sunny Goyalffe83f12014-08-14 17:39:34 -0700256 sb.append(((AppWidgetProviderInfo) o).toString());
Michael Jurka05713af2013-01-23 12:39:24 +0100257 output = sb.toString();
258 sb.setLength(0);
259 } else {
260 sb.append(SHORTCUT_PREFIX);
Michael Jurka05713af2013-01-23 12:39:24 +0100261 ResolveInfo info = (ResolveInfo) o;
262 sb.append(new ComponentName(info.activityInfo.packageName,
263 info.activityInfo.name).flattenToString());
264 output = sb.toString();
265 sb.setLength(0);
266 }
267 return output;
268 }
269
270 private String getObjectPackage(Object o) {
271 if (o instanceof AppWidgetProviderInfo) {
272 return ((AppWidgetProviderInfo) o).provider.getPackageName();
273 } else {
274 ResolveInfo info = (ResolveInfo) o;
275 return info.activityInfo.packageName;
276 }
277 }
278
Adam Cohen091440a2015-03-18 14:16:05 -0700279 @Thunk void writeToDb(Object o, Bitmap preview) {
Michael Jurka05713af2013-01-23 12:39:24 +0100280 String name = getObjectName(o);
281 SQLiteDatabase db = mDb.getWritableDatabase();
282 ContentValues values = new ContentValues();
283
Michael Jurkad9cb4a12013-03-19 12:01:06 +0100284 values.put(CacheDb.COLUMN_NAME, name);
Michael Jurka05713af2013-01-23 12:39:24 +0100285 ByteArrayOutputStream stream = new ByteArrayOutputStream();
286 preview.compress(Bitmap.CompressFormat.PNG, 100, stream);
Michael Jurkad9cb4a12013-03-19 12:01:06 +0100287 values.put(CacheDb.COLUMN_PREVIEW_BITMAP, stream.toByteArray());
288 values.put(CacheDb.COLUMN_SIZE, mSize);
Michael Jurka6e27f642013-12-10 13:40:30 +0100289 try {
290 db.insert(CacheDb.TABLE_NAME, null, values);
291 } catch (SQLiteDiskIOException e) {
292 recreateDb();
Adrian Roos1f375ab2014-04-28 18:26:38 +0200293 } catch (SQLiteCantOpenDatabaseException e) {
294 dumpOpenFiles();
295 throw e;
Michael Jurka6e27f642013-12-10 13:40:30 +0100296 }
Michael Jurka05713af2013-01-23 12:39:24 +0100297 }
298
Michael Jurka8ff02ca2013-11-01 14:19:27 +0100299 private void clearDb() {
300 SQLiteDatabase db = mDb.getWritableDatabase();
301 // Delete everything
Michael Jurka6e27f642013-12-10 13:40:30 +0100302 try {
303 db.delete(CacheDb.TABLE_NAME, null, null);
304 } catch (SQLiteDiskIOException e) {
Adrian Roos1f375ab2014-04-28 18:26:38 +0200305 } catch (SQLiteCantOpenDatabaseException e) {
306 dumpOpenFiles();
307 throw e;
Michael Jurka6e27f642013-12-10 13:40:30 +0100308 }
Michael Jurka8ff02ca2013-11-01 14:19:27 +0100309 }
310
Michael Jurkaeb1bb922013-09-26 11:29:01 -0700311 public static void removePackageFromDb(final CacheDb cacheDb, final String packageName) {
Michael Jurka05713af2013-01-23 12:39:24 +0100312 synchronized(sInvalidPackages) {
313 sInvalidPackages.add(packageName);
314 }
315 new AsyncTask<Void, Void, Void>() {
316 public Void doInBackground(Void ... args) {
Michael Jurkad9cb4a12013-03-19 12:01:06 +0100317 SQLiteDatabase db = cacheDb.getWritableDatabase();
Michael Jurka6e27f642013-12-10 13:40:30 +0100318 try {
319 db.delete(CacheDb.TABLE_NAME,
320 CacheDb.COLUMN_NAME + " LIKE ? OR " +
321 CacheDb.COLUMN_NAME + " LIKE ?", // SELECT query
322 new String[] {
323 WIDGET_PREFIX + packageName + "/%",
324 SHORTCUT_PREFIX + packageName + "/%"
325 } // args to SELECT query
326 );
327 } catch (SQLiteDiskIOException e) {
Adrian Roos1f375ab2014-04-28 18:26:38 +0200328 } catch (SQLiteCantOpenDatabaseException e) {
329 dumpOpenFiles();
330 throw e;
Michael Jurka6e27f642013-12-10 13:40:30 +0100331 }
Michael Jurka05713af2013-01-23 12:39:24 +0100332 synchronized(sInvalidPackages) {
333 sInvalidPackages.remove(packageName);
334 }
335 return null;
336 }
337 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
338 }
339
Sunny Goyalffe83f12014-08-14 17:39:34 -0700340 private static void removeItemFromDb(final CacheDb cacheDb, final String objectName) {
Michael Jurkaeb1bb922013-09-26 11:29:01 -0700341 new AsyncTask<Void, Void, Void>() {
342 public Void doInBackground(Void ... args) {
343 SQLiteDatabase db = cacheDb.getWritableDatabase();
Michael Jurka6e27f642013-12-10 13:40:30 +0100344 try {
345 db.delete(CacheDb.TABLE_NAME,
346 CacheDb.COLUMN_NAME + " = ? ", // SELECT query
347 new String[] { objectName }); // args to SELECT query
348 } catch (SQLiteDiskIOException e) {
Adrian Roos1f375ab2014-04-28 18:26:38 +0200349 } catch (SQLiteCantOpenDatabaseException e) {
350 dumpOpenFiles();
351 throw e;
Michael Jurka6e27f642013-12-10 13:40:30 +0100352 }
Michael Jurkaeb1bb922013-09-26 11:29:01 -0700353 return null;
354 }
355 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
356 }
357
Michael Jurka05713af2013-01-23 12:39:24 +0100358 private Bitmap readFromDb(String name, Bitmap b) {
359 if (mCachedSelectQuery == null) {
Michael Jurkad9cb4a12013-03-19 12:01:06 +0100360 mCachedSelectQuery = CacheDb.COLUMN_NAME + " = ? AND " +
361 CacheDb.COLUMN_SIZE + " = ?";
Michael Jurka05713af2013-01-23 12:39:24 +0100362 }
363 SQLiteDatabase db = mDb.getReadableDatabase();
Michael Jurka6e27f642013-12-10 13:40:30 +0100364 Cursor result;
365 try {
366 result = db.query(CacheDb.TABLE_NAME,
367 new String[] { CacheDb.COLUMN_PREVIEW_BITMAP }, // cols to return
368 mCachedSelectQuery, // select query
369 new String[] { name, mSize }, // args to select query
370 null,
371 null,
372 null,
373 null);
374 } catch (SQLiteDiskIOException e) {
375 recreateDb();
376 return null;
Adrian Roos1f375ab2014-04-28 18:26:38 +0200377 } catch (SQLiteCantOpenDatabaseException e) {
378 dumpOpenFiles();
379 throw e;
Michael Jurka6e27f642013-12-10 13:40:30 +0100380 }
Michael Jurka05713af2013-01-23 12:39:24 +0100381 if (result.getCount() > 0) {
382 result.moveToFirst();
383 byte[] blob = result.getBlob(0);
384 result.close();
Sunny Goyal4cad7532015-03-18 15:56:30 -0700385 final BitmapFactory.Options opts = new BitmapFactory.Options();
Michael Jurka05713af2013-01-23 12:39:24 +0100386 opts.inBitmap = b;
387 opts.inSampleSize = 1;
Michael Jurkaeb1bb922013-09-26 11:29:01 -0700388 try {
389 return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts);
390 } catch (IllegalArgumentException e) {
391 removeItemFromDb(mDb, name);
392 return null;
393 }
Michael Jurka05713af2013-01-23 12:39:24 +0100394 } else {
395 result.close();
396 return null;
397 }
398 }
399
Sunny Goyalffe83f12014-08-14 17:39:34 -0700400 private Bitmap generatePreview(Object info, Bitmap preview) {
Michael Jurka05713af2013-01-23 12:39:24 +0100401 if (preview != null &&
Michael Jurka3f4e0702013-02-05 11:21:28 +0100402 (preview.getWidth() != mPreviewBitmapWidth ||
403 preview.getHeight() != mPreviewBitmapHeight)) {
Michael Jurka05713af2013-01-23 12:39:24 +0100404 throw new RuntimeException("Improperly sized bitmap passed as argument");
405 }
Adam Cohen59400422014-03-05 18:07:04 -0800406 if (info instanceof LauncherAppWidgetProviderInfo) {
407 return generateWidgetPreview((LauncherAppWidgetProviderInfo) info, preview);
Michael Jurka05713af2013-01-23 12:39:24 +0100408 } else {
409 return generateShortcutPreview(
Michael Jurka3f4e0702013-02-05 11:21:28 +0100410 (ResolveInfo) info, mPreviewBitmapWidth, mPreviewBitmapHeight, preview);
Michael Jurka05713af2013-01-23 12:39:24 +0100411 }
412 }
413
Adam Cohen59400422014-03-05 18:07:04 -0800414 public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info, Bitmap preview) {
415 int maxWidth = maxWidthForWidgetPreview(info.spanX);
Sunny Goyal4cad7532015-03-18 15:56:30 -0700416 return generateWidgetPreview(info, maxWidth, preview, null);
Michael Jurka05713af2013-01-23 12:39:24 +0100417 }
418
419 public int maxWidthForWidgetPreview(int spanX) {
Sunny Goyal4cad7532015-03-18 15:56:30 -0700420 return Math.min(mPreviewBitmapWidth, spanX * mCellWidth);
Michael Jurka05713af2013-01-23 12:39:24 +0100421 }
422
Sunny Goyal4cad7532015-03-18 15:56:30 -0700423 public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info,
424 int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) {
Michael Jurka05713af2013-01-23 12:39:24 +0100425 // Load the preview image if possible
Michael Jurka05713af2013-01-23 12:39:24 +0100426 if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE;
Michael Jurka05713af2013-01-23 12:39:24 +0100427
428 Drawable drawable = null;
Sunny Goyalffe83f12014-08-14 17:39:34 -0700429 if (info.previewImage != 0) {
430 drawable = mManager.loadPreview(info);
Adrian Roosfa9ffc22014-05-12 15:59:59 +0200431 if (drawable != null) {
432 drawable = mutateOnMainThread(drawable);
433 } else {
Michael Jurka05713af2013-01-23 12:39:24 +0100434 Log.w(TAG, "Can't load widget preview drawable 0x" +
Sunny Goyalffe83f12014-08-14 17:39:34 -0700435 Integer.toHexString(info.previewImage) + " for provider: " + info.provider);
Michael Jurka05713af2013-01-23 12:39:24 +0100436 }
437 }
438
Sunny Goyal4cad7532015-03-18 15:56:30 -0700439 final boolean widgetPreviewExists = (drawable != null);
440 final int spanX = info.spanX < 1 ? 1 : info.spanX;
441 final int spanY = info.spanY < 1 ? 1 : info.spanY;
442
Michael Jurka05713af2013-01-23 12:39:24 +0100443 int previewWidth;
444 int previewHeight;
Sunny Goyal4cad7532015-03-18 15:56:30 -0700445 Bitmap tileBitmap = null;
446
Michael Jurka05713af2013-01-23 12:39:24 +0100447 if (widgetPreviewExists) {
448 previewWidth = drawable.getIntrinsicWidth();
449 previewHeight = drawable.getIntrinsicHeight();
450 } else {
451 // Generate a preview image if we couldn't load one
Sunny Goyal4cad7532015-03-18 15:56:30 -0700452 tileBitmap = ((BitmapDrawable) mContext.getResources().getDrawable(
453 R.drawable.widget_tile)).getBitmap();
454 previewWidth = tileBitmap.getWidth() * spanX;
455 previewHeight = tileBitmap.getHeight() * spanY;
Michael Jurka05713af2013-01-23 12:39:24 +0100456 }
457
458 // Scale to fit width only - let the widget preview be clipped in the
459 // vertical dimension
460 float scale = 1f;
461 if (preScaledWidthOut != null) {
462 preScaledWidthOut[0] = previewWidth;
463 }
464 if (previewWidth > maxPreviewWidth) {
465 scale = maxPreviewWidth / (float) previewWidth;
466 }
467 if (scale != 1f) {
468 previewWidth = (int) (scale * previewWidth);
469 previewHeight = (int) (scale * previewHeight);
470 }
471
472 // 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 -0700473 final Canvas c = new Canvas();
Michael Jurka05713af2013-01-23 12:39:24 +0100474 if (preview == null) {
475 preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);
Sunny Goyal4cad7532015-03-18 15:56:30 -0700476 c.setBitmap(preview);
477 } else {
478 // Reusing bitmap. Clear it.
479 c.setBitmap(preview);
480 c.drawColor(0, PorterDuff.Mode.CLEAR);
Michael Jurka05713af2013-01-23 12:39:24 +0100481 }
482
483 // Draw the scaled preview into the final bitmap
484 int x = (preview.getWidth() - previewWidth) / 2;
485 if (widgetPreviewExists) {
Sunny Goyal4cad7532015-03-18 15:56:30 -0700486 drawable.setBounds(x, 0, x + previewWidth, previewHeight);
487 drawable.draw(c);
Michael Jurka05713af2013-01-23 12:39:24 +0100488 } else {
Sunny Goyal4cad7532015-03-18 15:56:30 -0700489 final Paint p = new Paint();
490 p.setFilterBitmap(true);
Michael Jurka05713af2013-01-23 12:39:24 +0100491
Sunny Goyal4cad7532015-03-18 15:56:30 -0700492 // draw the spanX x spanY tiles
493 final Rect src = new Rect(0, 0, tileBitmap.getWidth(), tileBitmap.getHeight());
494
495 float tileW = scale * tileBitmap.getWidth();
496 float tileH = scale * tileBitmap.getHeight();
497 final RectF dst = new RectF(0, 0, tileW, tileH);
498
499 float tx = x;
500 for (int i = 0; i < spanX; i++, tx += tileW) {
501 float ty = 0;
502 for (int j = 0; j < spanY; j++, ty += tileH) {
503 dst.offsetTo(tx, ty);
504 c.drawBitmap(tileBitmap, src, dst, p);
505 }
Michael Jurka05713af2013-01-23 12:39:24 +0100506 }
Sunny Goyal4cad7532015-03-18 15:56:30 -0700507
508 // Draw the icon in the top left corner
509 // TODO: use top right for RTL
510 int minOffset = (int) (mAppIconSize * WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE);
511 int smallestSide = Math.min(previewWidth, previewHeight);
512 float iconScale = Math.min((float) smallestSide / (mAppIconSize + 2 * minOffset), scale);
513
514 try {
515 Drawable icon = mutateOnMainThread(mManager.loadIcon(info, mIconCache));
516 if (icon != null) {
517 int hoffset = (int) ((tileW - mAppIconSize * iconScale) / 2) + x;
518 int yoffset = (int) ((tileH - mAppIconSize * iconScale) / 2);
519 icon.setBounds(hoffset, yoffset,
520 hoffset + (int) (mAppIconSize * iconScale),
521 yoffset + (int) (mAppIconSize * iconScale));
522 icon.draw(c);
523 }
524 } catch (Resources.NotFoundException e) { }
Michael Jurka05713af2013-01-23 12:39:24 +0100525 c.setBitmap(null);
526 }
Sunny Goyalffe83f12014-08-14 17:39:34 -0700527 return mManager.getBadgeBitmap(info, preview);
Michael Jurka05713af2013-01-23 12:39:24 +0100528 }
529
530 private Bitmap generateShortcutPreview(
531 ResolveInfo info, int maxWidth, int maxHeight, Bitmap preview) {
Sunny Goyal4cad7532015-03-18 15:56:30 -0700532 final Canvas c = new Canvas();
533 if (preview == null) {
Michael Jurka05713af2013-01-23 12:39:24 +0100534 preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888);
Sunny Goyal4cad7532015-03-18 15:56:30 -0700535 c.setBitmap(preview);
536 } else if (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight) {
537 throw new RuntimeException("Improperly sized bitmap passed as argument");
538 } else {
539 // Reusing bitmap. Clear it.
540 c.setBitmap(preview);
541 c.drawColor(0, PorterDuff.Mode.CLEAR);
Michael Jurka05713af2013-01-23 12:39:24 +0100542 }
543
Sunny Goyal4cad7532015-03-18 15:56:30 -0700544 Drawable icon = mutateOnMainThread(mIconCache.getFullResIcon(info.activityInfo));
545 icon.setFilterBitmap(true);
546
Michael Jurka05713af2013-01-23 12:39:24 +0100547 // Draw a desaturated/scaled version of the icon in the background as a watermark
Sunny Goyal4cad7532015-03-18 15:56:30 -0700548 ColorMatrix colorMatrix = new ColorMatrix();
549 colorMatrix.setSaturation(0);
550 icon.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
551 icon.setAlpha((int) (255 * 0.06f));
552
553 Resources res = mContext.getResources();
554 int paddingTop = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top);
555 int paddingLeft = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left);
556 int paddingRight = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right);
557 int scaledIconWidth = (maxWidth - paddingLeft - paddingRight);
558 icon.setBounds(paddingLeft, paddingTop,
559 paddingLeft + scaledIconWidth, paddingTop + scaledIconWidth);
560 icon.draw(c);
561
562 // Draw the final icon at top left corner.
563 // TODO: use top right for RTL
564 icon.setAlpha(255);
565 icon.setColorFilter(null);
566 icon.setBounds(0, 0, mAppIconSize, mAppIconSize);
567 icon.draw(c);
568
Michael Jurka05713af2013-01-23 12:39:24 +0100569 c.setBitmap(null);
Michael Jurka05713af2013-01-23 12:39:24 +0100570 return preview;
571 }
572
Adrian Roos65d60e22014-04-15 21:07:49 +0200573 private Drawable mutateOnMainThread(final Drawable drawable) {
574 try {
575 return mMainThreadExecutor.submit(new Callable<Drawable>() {
576 @Override
577 public Drawable call() throws Exception {
578 return drawable.mutate();
579 }
580 }).get();
581 } catch (InterruptedException e) {
582 Thread.currentThread().interrupt();
583 throw new RuntimeException(e);
584 } catch (ExecutionException e) {
585 throw new RuntimeException(e);
586 }
587 }
Adrian Roos1f375ab2014-04-28 18:26:38 +0200588
589 private static final int MAX_OPEN_FILES = 1024;
590 private static final int SAMPLE_RATE = 23;
591 /**
592 * Dumps all files that are open in this process without allocating a file descriptor.
593 */
Adam Cohen091440a2015-03-18 14:16:05 -0700594 @Thunk static void dumpOpenFiles() {
Adrian Roos1f375ab2014-04-28 18:26:38 +0200595 try {
596 Log.i(TAG, "DUMP OF OPEN FILES (sample rate: 1 every " + SAMPLE_RATE + "):");
597 final String TYPE_APK = "apk";
598 final String TYPE_JAR = "jar";
599 final String TYPE_PIPE = "pipe";
600 final String TYPE_SOCKET = "socket";
601 final String TYPE_DB = "db";
602 final String TYPE_ANON_INODE = "anon_inode";
603 final String TYPE_DEV = "dev";
604 final String TYPE_NON_FS = "non-fs";
605 final String TYPE_OTHER = "other";
606 List<String> types = Arrays.asList(TYPE_APK, TYPE_JAR, TYPE_PIPE, TYPE_SOCKET, TYPE_DB,
607 TYPE_ANON_INODE, TYPE_DEV, TYPE_NON_FS, TYPE_OTHER);
608 int[] count = new int[types.size()];
609 int[] duplicates = new int[types.size()];
610 HashSet<String> files = new HashSet<String>();
611 int total = 0;
612 for (int i = 0; i < MAX_OPEN_FILES; i++) {
613 // This is a gigantic hack but unfortunately the only way to resolve an fd
614 // to a file name. Note that we have to loop over all possible fds because
615 // reading the directory would require allocating a new fd. The kernel is
616 // currently implemented such that no fd is larger then the current rlimit,
617 // which is why it's safe to loop over them in such a way.
618 String fd = "/proc/self/fd/" + i;
619 try {
620 // getCanonicalPath() uses readlink behind the scene which doesn't require
621 // a file descriptor.
622 String resolved = new File(fd).getCanonicalPath();
623 int type = types.indexOf(TYPE_OTHER);
624 if (resolved.startsWith("/dev/")) {
625 type = types.indexOf(TYPE_DEV);
626 } else if (resolved.endsWith(".apk")) {
627 type = types.indexOf(TYPE_APK);
628 } else if (resolved.endsWith(".jar")) {
629 type = types.indexOf(TYPE_JAR);
630 } else if (resolved.contains("/fd/pipe:")) {
631 type = types.indexOf(TYPE_PIPE);
632 } else if (resolved.contains("/fd/socket:")) {
633 type = types.indexOf(TYPE_SOCKET);
634 } else if (resolved.contains("/fd/anon_inode:")) {
635 type = types.indexOf(TYPE_ANON_INODE);
636 } else if (resolved.endsWith(".db") || resolved.contains("/databases/")) {
637 type = types.indexOf(TYPE_DB);
638 } else if (resolved.startsWith("/proc/") && resolved.contains("/fd/")) {
639 // Those are the files that don't point anywhere on the file system.
640 // getCanonicalPath() wrongly interprets these as relative symlinks and
641 // resolves them within /proc/<pid>/fd/.
642 type = types.indexOf(TYPE_NON_FS);
643 }
644 count[type]++;
645 total++;
646 if (files.contains(resolved)) {
647 duplicates[type]++;
648 }
649 files.add(resolved);
650 if (total % SAMPLE_RATE == 0) {
651 Log.i(TAG, " fd " + i + ": " + resolved
652 + " (" + types.get(type) + ")");
653 }
654 } catch (IOException e) {
655 // Ignoring exceptions for non-existing file descriptors.
656 }
657 }
658 for (int i = 0; i < types.size(); i++) {
659 Log.i(TAG, String.format("Open %10s files: %4d total, %4d duplicates",
660 types.get(i), count[i], duplicates[i]));
661 }
662 } catch (Throwable t) {
663 // Catch everything. This is called from an exception handler that we shouldn't upset.
664 Log.e(TAG, "Unable to log open files.", t);
665 }
666 }
Michael Jurka05713af2013-01-23 12:39:24 +0100667}