1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.icons.cache;
17 
18 import static com.android.launcher3.icons.BaseIconFactory.getFullResDefaultActivityIcon;
19 import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON;
20 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
21 
22 import android.content.ComponentName;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.content.pm.ActivityInfo;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageInfo;
28 import android.content.pm.PackageManager;
29 import android.content.pm.PackageManager.NameNotFoundException;
30 import android.content.res.Resources;
31 import android.database.Cursor;
32 import android.database.sqlite.SQLiteDatabase;
33 import android.database.sqlite.SQLiteException;
34 import android.graphics.Bitmap;
35 import android.graphics.BitmapFactory;
36 import android.graphics.drawable.Drawable;
37 import android.os.Build;
38 import android.os.Handler;
39 import android.os.LocaleList;
40 import android.os.Looper;
41 import android.os.Process;
42 import android.os.UserHandle;
43 import android.text.TextUtils;
44 import android.util.Log;
45 
46 import androidx.annotation.NonNull;
47 import androidx.annotation.Nullable;
48 import androidx.annotation.VisibleForTesting;
49 
50 import com.android.launcher3.icons.BaseIconFactory;
51 import com.android.launcher3.icons.BitmapInfo;
52 import com.android.launcher3.icons.BitmapRenderer;
53 import com.android.launcher3.icons.GraphicsUtils;
54 import com.android.launcher3.util.ComponentKey;
55 import com.android.launcher3.util.SQLiteCacheHelper;
56 
57 import java.util.AbstractMap;
58 import java.util.Collections;
59 import java.util.HashMap;
60 import java.util.HashSet;
61 import java.util.Map;
62 import java.util.Set;
63 import java.util.function.Supplier;
64 
65 public abstract class BaseIconCache {
66 
67     private static final String TAG = "BaseIconCache";
68     private static final boolean DEBUG = false;
69 
70     private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
71 
72     // Empty class name is used for storing package default entry.
73     public static final String EMPTY_CLASS_NAME = ".";
74 
75     public static class CacheEntry {
76 
77         @NonNull
78         public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO;
79         public CharSequence title = "";
80         public CharSequence contentDescription = "";
81     }
82 
83     private final HashMap<UserHandle, BitmapInfo> mDefaultIcons = new HashMap<>();
84 
85     protected final Context mContext;
86     protected final PackageManager mPackageManager;
87 
88     private final Map<ComponentKey, CacheEntry> mCache;
89     protected final Handler mWorkerHandler;
90 
91     protected int mIconDpi;
92     protected IconDB mIconDb;
93     protected LocaleList mLocaleList = LocaleList.getEmptyLocaleList();
94     protected String mSystemState = "";
95 
96     private final String mDbFileName;
97     private final BitmapFactory.Options mDecodeOptions;
98     private final Looper mBgLooper;
99 
BaseIconCache(Context context, String dbFileName, Looper bgLooper, int iconDpi, int iconPixelSize, boolean inMemoryCache)100     public BaseIconCache(Context context, String dbFileName, Looper bgLooper,
101             int iconDpi, int iconPixelSize, boolean inMemoryCache) {
102         mContext = context;
103         mDbFileName = dbFileName;
104         mPackageManager = context.getPackageManager();
105         mBgLooper = bgLooper;
106         mWorkerHandler = new Handler(mBgLooper);
107 
108         if (inMemoryCache) {
109             mCache = new HashMap<>(INITIAL_ICON_CACHE_CAPACITY);
110         } else {
111             // Use a dummy cache
112             mCache = new AbstractMap<ComponentKey, CacheEntry>() {
113                 @Override
114                 public Set<Entry<ComponentKey, CacheEntry>> entrySet() {
115                     return Collections.emptySet();
116                 }
117 
118                 @Override
119                 public CacheEntry put(ComponentKey key, CacheEntry value) {
120                     return value;
121                 }
122             };
123         }
124 
125         if (BitmapRenderer.USE_HARDWARE_BITMAP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
126             mDecodeOptions = new BitmapFactory.Options();
127             mDecodeOptions.inPreferredConfig = Bitmap.Config.HARDWARE;
128         } else {
129             mDecodeOptions = null;
130         }
131 
132         updateSystemState();
133         mIconDpi = iconDpi;
134         mIconDb = new IconDB(context, dbFileName, iconPixelSize);
135     }
136 
137     /**
138      * Returns the persistable serial number for {@param user}. Subclass should implement proper
139      * caching strategy to avoid making binder call every time.
140      */
getSerialNumberForUser(UserHandle user)141     protected abstract long getSerialNumberForUser(UserHandle user);
142 
143     /**
144      * Return true if the given app is an instant app and should be badged appropriately.
145      */
isInstantApp(ApplicationInfo info)146     protected abstract boolean isInstantApp(ApplicationInfo info);
147 
148     /**
149      * Opens and returns an icon factory. The factory is recycled by the caller.
150      */
getIconFactory()151     protected abstract BaseIconFactory getIconFactory();
152 
updateIconParams(int iconDpi, int iconPixelSize)153     public void updateIconParams(int iconDpi, int iconPixelSize) {
154         mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize));
155     }
156 
updateIconParamsBg(int iconDpi, int iconPixelSize)157     private synchronized void updateIconParamsBg(int iconDpi, int iconPixelSize) {
158         mIconDpi = iconDpi;
159         mDefaultIcons.clear();
160         mIconDb.clear();
161         mIconDb.close();
162         mIconDb = new IconDB(mContext, mDbFileName, iconPixelSize);
163         mCache.clear();
164     }
165 
getFullResIcon(Resources resources, int iconId)166     private Drawable getFullResIcon(Resources resources, int iconId) {
167         if (resources != null && iconId != 0) {
168             try {
169                 return resources.getDrawableForDensity(iconId, mIconDpi);
170             } catch (Resources.NotFoundException e) { }
171         }
172         return getFullResDefaultActivityIcon(mIconDpi);
173     }
174 
getFullResIcon(String packageName, int iconId)175     public Drawable getFullResIcon(String packageName, int iconId) {
176         try {
177             return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId);
178         } catch (PackageManager.NameNotFoundException e) { }
179         return getFullResDefaultActivityIcon(mIconDpi);
180     }
181 
getFullResIcon(ActivityInfo info)182     public Drawable getFullResIcon(ActivityInfo info) {
183         try {
184             return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo),
185                     info.getIconResource());
186         } catch (PackageManager.NameNotFoundException e) { }
187         return getFullResDefaultActivityIcon(mIconDpi);
188     }
189 
makeDefaultIcon(UserHandle user)190     private BitmapInfo makeDefaultIcon(UserHandle user) {
191         try (BaseIconFactory li = getIconFactory()) {
192             return li.makeDefaultIcon(user);
193         }
194     }
195 
196     /**
197      * Remove any records for the supplied ComponentName.
198      */
remove(ComponentName componentName, UserHandle user)199     public synchronized void remove(ComponentName componentName, UserHandle user) {
200         mCache.remove(new ComponentKey(componentName, user));
201     }
202 
203     /**
204      * Remove any records for the supplied package name from memory.
205      */
removeFromMemCacheLocked(String packageName, UserHandle user)206     private void removeFromMemCacheLocked(String packageName, UserHandle user) {
207         HashSet<ComponentKey> forDeletion = new HashSet<>();
208         for (ComponentKey key: mCache.keySet()) {
209             if (key.componentName.getPackageName().equals(packageName)
210                     && key.user.equals(user)) {
211                 forDeletion.add(key);
212             }
213         }
214         for (ComponentKey condemned: forDeletion) {
215             mCache.remove(condemned);
216         }
217     }
218 
219     /**
220      * Removes the entries related to the given package in memory and persistent DB.
221      */
removeIconsForPkg(String packageName, UserHandle user)222     public synchronized void removeIconsForPkg(String packageName, UserHandle user) {
223         removeFromMemCacheLocked(packageName, user);
224         long userSerial = getSerialNumberForUser(user);
225         mIconDb.delete(
226                 IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?",
227                 new String[]{packageName + "/%", Long.toString(userSerial)});
228     }
229 
getUpdateHandler()230     public IconCacheUpdateHandler getUpdateHandler() {
231         updateSystemState();
232         return new IconCacheUpdateHandler(this);
233     }
234 
235     /**
236      * Refreshes the system state definition used to check the validity of the cache. It
237      * incorporates all the properties that can affect the cache like the list of enabled locale
238      * and system-version.
239      */
updateSystemState()240     private void updateSystemState() {
241         mLocaleList = mContext.getResources().getConfiguration().getLocales();
242         mSystemState = mLocaleList.toLanguageTags() + "," + Build.VERSION.SDK_INT;
243     }
244 
getIconSystemState(String packageName)245     protected String getIconSystemState(String packageName) {
246         return mSystemState;
247     }
248 
249     /**
250      * Adds an entry into the DB and the in-memory cache.
251      * @param replaceExisting if true, it will recreate the bitmap even if it already exists in
252      *                        the memory. This is useful then the previous bitmap was created using
253      *                        old data.
254      */
255     @VisibleForTesting
addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic, PackageInfo info, long userSerial, boolean replaceExisting)256     public synchronized <T> void addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic,
257             PackageInfo info, long userSerial, boolean replaceExisting) {
258         UserHandle user = cachingLogic.getUser(object);
259         ComponentName componentName = cachingLogic.getComponent(object);
260 
261         final ComponentKey key = new ComponentKey(componentName, user);
262         CacheEntry entry = null;
263         if (!replaceExisting) {
264             entry = mCache.get(key);
265             // We can't reuse the entry if the high-res icon is not present.
266             if (entry == null || entry.bitmap.isNullOrLowRes()) {
267                 entry = null;
268             }
269         }
270         if (entry == null) {
271             entry = new CacheEntry();
272             entry.bitmap = cachingLogic.loadIcon(mContext, object);
273         }
274         // Icon can't be loaded from cachingLogic, which implies alternative icon was loaded
275         // (e.g. fallback icon, default icon). So we drop here since there's no point in caching
276         // an empty entry.
277         if (entry.bitmap.isNullOrLowRes()) return;
278         entry.title = cachingLogic.getLabel(object);
279         entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
280         if (cachingLogic.addToMemCache()) mCache.put(key, entry);
281 
282         ContentValues values = newContentValues(entry.bitmap, entry.title.toString(),
283                 componentName.getPackageName(), cachingLogic.getKeywords(object, mLocaleList));
284         addIconToDB(values, componentName, info, userSerial,
285                 cachingLogic.getLastUpdatedTime(object, info));
286     }
287 
288     /**
289      * Updates {@param values} to contain versioning information and adds it to the DB.
290      * @param values {@link ContentValues} containing icon & title
291      */
addIconToDB(ContentValues values, ComponentName key, PackageInfo info, long userSerial, long lastUpdateTime)292     private void addIconToDB(ContentValues values, ComponentName key,
293             PackageInfo info, long userSerial, long lastUpdateTime) {
294         values.put(IconDB.COLUMN_COMPONENT, key.flattenToString());
295         values.put(IconDB.COLUMN_USER, userSerial);
296         values.put(IconDB.COLUMN_LAST_UPDATED, lastUpdateTime);
297         values.put(IconDB.COLUMN_VERSION, info.versionCode);
298         mIconDb.insertOrReplace(values);
299     }
300 
getDefaultIcon(UserHandle user)301     public synchronized BitmapInfo getDefaultIcon(UserHandle user) {
302         if (!mDefaultIcons.containsKey(user)) {
303             mDefaultIcons.put(user, makeDefaultIcon(user));
304         }
305         return mDefaultIcons.get(user);
306     }
307 
isDefaultIcon(BitmapInfo icon, UserHandle user)308     public boolean isDefaultIcon(BitmapInfo icon, UserHandle user) {
309         return getDefaultIcon(user).icon == icon.icon;
310     }
311 
312     /**
313      * Retrieves the entry from the cache. If the entry is not present, it creates a new entry.
314      * This method is not thread safe, it must be called from a synchronized method.
315      */
cacheLocked( @onNull ComponentName componentName, @NonNull UserHandle user, @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic, boolean usePackageIcon, boolean useLowResIcon)316     protected <T> CacheEntry cacheLocked(
317             @NonNull ComponentName componentName, @NonNull UserHandle user,
318             @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
319             boolean usePackageIcon, boolean useLowResIcon) {
320         assertWorkerThread();
321         ComponentKey cacheKey = new ComponentKey(componentName, user);
322         CacheEntry entry = mCache.get(cacheKey);
323         if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) {
324             entry = new CacheEntry();
325             if (cachingLogic.addToMemCache()) {
326                 mCache.put(cacheKey, entry);
327             }
328 
329             // Check the DB first.
330             T object = null;
331             boolean providerFetchedOnce = false;
332 
333             if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
334                 object = infoProvider.get();
335                 providerFetchedOnce = true;
336 
337                 if (object != null) {
338                     entry.bitmap = cachingLogic.loadIcon(mContext, object);
339                 } else {
340                     if (usePackageIcon) {
341                         CacheEntry packageEntry = getEntryForPackageLocked(
342                                 componentName.getPackageName(), user, false);
343                         if (packageEntry != null) {
344                             if (DEBUG) Log.d(TAG, "using package default icon for " +
345                                     componentName.toShortString());
346                             entry.bitmap = packageEntry.bitmap;
347                             entry.title = packageEntry.title;
348                             entry.contentDescription = packageEntry.contentDescription;
349                         }
350                     }
351                     if (entry.bitmap == null) {
352                         if (DEBUG) Log.d(TAG, "using default icon for " +
353                                 componentName.toShortString());
354                         entry.bitmap = getDefaultIcon(user);
355                     }
356                 }
357             }
358 
359             if (TextUtils.isEmpty(entry.title)) {
360                 if (object == null && !providerFetchedOnce) {
361                     object = infoProvider.get();
362                     providerFetchedOnce = true;
363                 }
364                 if (object != null) {
365                     entry.title = cachingLogic.getLabel(object);
366                     entry.contentDescription = mPackageManager.getUserBadgedLabel(
367                             cachingLogic.getDescription(object, entry.title), user);
368                 }
369             }
370         }
371         return entry;
372     }
373 
clear()374     public synchronized void clear() {
375         assertWorkerThread();
376         mIconDb.clear();
377     }
378 
379     /**
380      * Adds a default package entry in the cache. This entry is not persisted and will be removed
381      * when the cache is flushed.
382      */
cachePackageInstallInfo(String packageName, UserHandle user, Bitmap icon, CharSequence title)383     protected synchronized void cachePackageInstallInfo(String packageName, UserHandle user,
384             Bitmap icon, CharSequence title) {
385         removeFromMemCacheLocked(packageName, user);
386 
387         ComponentKey cacheKey = getPackageKey(packageName, user);
388         CacheEntry entry = mCache.get(cacheKey);
389 
390         // For icon caching, do not go through DB. Just update the in-memory entry.
391         if (entry == null) {
392             entry = new CacheEntry();
393         }
394         if (!TextUtils.isEmpty(title)) {
395             entry.title = title;
396         }
397         if (icon != null) {
398             BaseIconFactory li = getIconFactory();
399             entry.bitmap = li.createIconBitmap(icon);
400             li.close();
401         }
402         if (!TextUtils.isEmpty(title) && entry.bitmap.icon != null) {
403             mCache.put(cacheKey, entry);
404         }
405     }
406 
getPackageKey(String packageName, UserHandle user)407     private static ComponentKey getPackageKey(String packageName, UserHandle user) {
408         ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME);
409         return new ComponentKey(cn, user);
410     }
411 
412     /**
413      * Gets an entry for the package, which can be used as a fallback entry for various components.
414      * This method is not thread safe, it must be called from a synchronized method.
415      */
getEntryForPackageLocked(String packageName, UserHandle user, boolean useLowResIcon)416     protected CacheEntry getEntryForPackageLocked(String packageName, UserHandle user,
417             boolean useLowResIcon) {
418         assertWorkerThread();
419         ComponentKey cacheKey = getPackageKey(packageName, user);
420         CacheEntry entry = mCache.get(cacheKey);
421 
422         if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) {
423             entry = new CacheEntry();
424             boolean entryUpdated = true;
425 
426             // Check the DB first.
427             if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
428                 try {
429                     int flags = Process.myUserHandle().equals(user) ? 0 :
430                             PackageManager.GET_UNINSTALLED_PACKAGES;
431                     PackageInfo info = mPackageManager.getPackageInfo(packageName, flags);
432                     ApplicationInfo appInfo = info.applicationInfo;
433                     if (appInfo == null) {
434                         throw new NameNotFoundException("ApplicationInfo is null");
435                     }
436 
437                     BaseIconFactory li = getIconFactory();
438                     // Load the full res icon for the application, but if useLowResIcon is set, then
439                     // only keep the low resolution icon instead of the larger full-sized icon
440                     BitmapInfo iconInfo = li.createBadgedIconBitmap(
441                             appInfo.loadIcon(mPackageManager), user, appInfo.targetSdkVersion,
442                             isInstantApp(appInfo));
443                     li.close();
444 
445                     entry.title = appInfo.loadLabel(mPackageManager);
446                     entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
447                     entry.bitmap = BitmapInfo.of(
448                             useLowResIcon ? LOW_RES_ICON : iconInfo.icon, iconInfo.color);
449 
450                     // Add the icon in the DB here, since these do not get written during
451                     // package updates.
452                     ContentValues values = newContentValues(
453                             iconInfo, entry.title.toString(), packageName, null);
454                     addIconToDB(values, cacheKey.componentName, info, getSerialNumberForUser(user),
455                             info.lastUpdateTime);
456 
457                 } catch (NameNotFoundException e) {
458                     if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
459                     entryUpdated = false;
460                 }
461             }
462 
463             // Only add a filled-out entry to the cache
464             if (entryUpdated) {
465                 mCache.put(cacheKey, entry);
466             }
467         }
468         return entry;
469     }
470 
getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes)471     protected boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
472         Cursor c = null;
473         try {
474             c = mIconDb.query(
475                     lowRes ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES,
476                     IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
477                     new String[]{
478                             cacheKey.componentName.flattenToString(),
479                             Long.toString(getSerialNumberForUser(cacheKey.user))});
480             if (c.moveToNext()) {
481                 // Set the alpha to be 255, so that we never have a wrong color
482                 entry.bitmap = BitmapInfo.of(LOW_RES_ICON, setColorAlphaBound(c.getInt(0), 255));
483                 entry.title = c.getString(1);
484                 if (entry.title == null) {
485                     entry.title = "";
486                     entry.contentDescription = "";
487                 } else {
488                     entry.contentDescription = mPackageManager.getUserBadgedLabel(
489                             entry.title, cacheKey.user);
490                 }
491 
492                 if (!lowRes) {
493                     byte[] data = c.getBlob(2);
494                     try {
495                         entry.bitmap = BitmapInfo.of(
496                                 BitmapFactory.decodeByteArray(data, 0, data.length, mDecodeOptions),
497                                 entry.bitmap.color);
498                     } catch (Exception e) { }
499                 }
500                 return true;
501             }
502         } catch (SQLiteException e) {
503             Log.d(TAG, "Error reading icon cache", e);
504         } finally {
505             if (c != null) {
506                 c.close();
507             }
508         }
509         return false;
510     }
511 
512     /**
513      * Returns a cursor for an arbitrary query to the cache db
514      */
queryCacheDb(String[] columns, String selection, String[] selectionArgs)515     public synchronized Cursor queryCacheDb(String[] columns, String selection,
516             String[] selectionArgs) {
517         return mIconDb.query(columns, selection, selectionArgs);
518     }
519 
520     /**
521      * Cache class to store the actual entries on disk
522      */
523     public static final class IconDB extends SQLiteCacheHelper {
524         private static final int RELEASE_VERSION = 27;
525 
526         public static final String TABLE_NAME = "icons";
527         public static final String COLUMN_ROWID = "rowid";
528         public static final String COLUMN_COMPONENT = "componentName";
529         public static final String COLUMN_USER = "profileId";
530         public static final String COLUMN_LAST_UPDATED = "lastUpdated";
531         public static final String COLUMN_VERSION = "version";
532         public static final String COLUMN_ICON = "icon";
533         public static final String COLUMN_ICON_COLOR = "icon_color";
534         public static final String COLUMN_LABEL = "label";
535         public static final String COLUMN_SYSTEM_STATE = "system_state";
536         public static final String COLUMN_KEYWORDS = "keywords";
537 
538         public static final String[] COLUMNS_HIGH_RES = new String[] {
539                 IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL, IconDB.COLUMN_ICON };
540         public static final String[] COLUMNS_LOW_RES = new String[] {
541                 IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL };
542 
IconDB(Context context, String dbFileName, int iconPixelSize)543         public IconDB(Context context, String dbFileName, int iconPixelSize) {
544             super(context, dbFileName, (RELEASE_VERSION << 16) + iconPixelSize, TABLE_NAME);
545         }
546 
547         @Override
onCreateTable(SQLiteDatabase db)548         protected void onCreateTable(SQLiteDatabase db) {
549             db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ("
550                     + COLUMN_COMPONENT + " TEXT NOT NULL, "
551                     + COLUMN_USER + " INTEGER NOT NULL, "
552                     + COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, "
553                     + COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, "
554                     + COLUMN_ICON + " BLOB, "
555                     + COLUMN_ICON_COLOR + " INTEGER NOT NULL DEFAULT 0, "
556                     + COLUMN_LABEL + " TEXT, "
557                     + COLUMN_SYSTEM_STATE + " TEXT, "
558                     + COLUMN_KEYWORDS + " TEXT, "
559                     + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") "
560                     + ");");
561         }
562     }
563 
newContentValues(BitmapInfo bitmapInfo, String label, String packageName, @Nullable String keywords)564     private ContentValues newContentValues(BitmapInfo bitmapInfo, String label,
565             String packageName, @Nullable String keywords) {
566         ContentValues values = new ContentValues();
567         values.put(IconDB.COLUMN_ICON,
568                 bitmapInfo.isLowRes() ? null : GraphicsUtils.flattenBitmap(bitmapInfo.icon));
569         values.put(IconDB.COLUMN_ICON_COLOR, bitmapInfo.color);
570 
571         values.put(IconDB.COLUMN_LABEL, label);
572         values.put(IconDB.COLUMN_SYSTEM_STATE, getIconSystemState(packageName));
573         values.put(IconDB.COLUMN_KEYWORDS, keywords);
574         return values;
575     }
576 
assertWorkerThread()577     private void assertWorkerThread() {
578         if (Looper.myLooper() != mBgLooper) {
579             throw new IllegalStateException("Cache accessed on wrong thread " + Looper.myLooper());
580         }
581     }
582 }
583