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