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