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