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 android.graphics.BitmapFactory.decodeByteArray;
19 
20 import static com.android.launcher3.icons.BaseIconFactory.getFullResDefaultActivityIcon;
21 import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON;
22 import static com.android.launcher3.icons.GraphicsUtils.flattenBitmap;
23 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
24 
25 import static java.util.Objects.requireNonNull;
26 
27 import android.content.ComponentName;
28 import android.content.ContentValues;
29 import android.content.Context;
30 import android.content.pm.ActivityInfo;
31 import android.content.pm.ApplicationInfo;
32 import android.content.pm.PackageInfo;
33 import android.content.pm.PackageManager;
34 import android.content.pm.PackageManager.NameNotFoundException;
35 import android.content.res.Resources;
36 import android.database.Cursor;
37 import android.database.sqlite.SQLiteDatabase;
38 import android.database.sqlite.SQLiteException;
39 import android.graphics.Bitmap;
40 import android.graphics.Bitmap.Config;
41 import android.graphics.BitmapFactory;
42 import android.graphics.drawable.Drawable;
43 import android.os.Build;
44 import android.os.Handler;
45 import android.os.LocaleList;
46 import android.os.Looper;
47 import android.os.Process;
48 import android.os.Trace;
49 import android.os.UserHandle;
50 import android.text.TextUtils;
51 import android.util.Log;
52 import android.util.SparseArray;
53 
54 import androidx.annotation.NonNull;
55 import androidx.annotation.Nullable;
56 import androidx.annotation.VisibleForTesting;
57 import androidx.annotation.WorkerThread;
58 
59 import com.android.launcher3.icons.BaseIconFactory;
60 import com.android.launcher3.icons.BaseIconFactory.IconOptions;
61 import com.android.launcher3.icons.BitmapInfo;
62 import com.android.launcher3.util.ComponentKey;
63 import com.android.launcher3.util.FlagOp;
64 import com.android.launcher3.util.SQLiteCacheHelper;
65 
66 import java.nio.ByteBuffer;
67 import java.util.AbstractMap;
68 import java.util.Arrays;
69 import java.util.Collections;
70 import java.util.HashMap;
71 import java.util.HashSet;
72 import java.util.Map;
73 import java.util.Set;
74 import java.util.function.Supplier;
75 
76 public abstract class BaseIconCache {
77 
78     private static final String TAG = "BaseIconCache";
79     private static final boolean DEBUG = false;
80 
81     private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
82     // A format string which returns the original string as is.
83     private static final String IDENTITY_FORMAT_STRING = "%1$s";
84 
85     // Empty class name is used for storing package default entry.
86     public static final String EMPTY_CLASS_NAME = ".";
87 
88     public static class CacheEntry {
89 
90         @NonNull
91         public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO;
92         @NonNull
93         public CharSequence title = "";
94         @NonNull
95         public CharSequence contentDescription = "";
96     }
97 
98     @NonNull
99     protected final Context mContext;
100 
101     @NonNull
102     protected final PackageManager mPackageManager;
103 
104     @NonNull
105     private final Map<ComponentKey, CacheEntry> mCache;
106 
107     @NonNull
108     protected final Handler mWorkerHandler;
109 
110     protected int mIconDpi;
111 
112     @NonNull
113     protected IconDB mIconDb;
114 
115     @NonNull
116     protected LocaleList mLocaleList = LocaleList.getEmptyLocaleList();
117 
118     @NonNull
119     protected String mSystemState = "";
120 
121     @Nullable
122     private BitmapInfo mDefaultIcon;
123 
124     @NonNull
125     private final SparseArray<FlagOp> mUserFlagOpMap = new SparseArray<>();
126 
127     private final SparseArray<String> mUserFormatString = new SparseArray<>();
128 
129     @Nullable
130     private final String mDbFileName;
131 
132     @NonNull
133     private final Looper mBgLooper;
134 
135     private volatile boolean mIconUpdateInProgress = false;
136 
BaseIconCache(@onNull final Context context, @Nullable final String dbFileName, @NonNull final Looper bgLooper, final int iconDpi, final int iconPixelSize, final boolean inMemoryCache)137     public BaseIconCache(@NonNull final Context context, @Nullable final String dbFileName,
138             @NonNull final Looper bgLooper, final int iconDpi, final int iconPixelSize,
139             final boolean inMemoryCache) {
140         mContext = context;
141         mDbFileName = dbFileName;
142         mPackageManager = context.getPackageManager();
143         mBgLooper = bgLooper;
144         mWorkerHandler = new Handler(mBgLooper);
145 
146         if (inMemoryCache) {
147             mCache = new HashMap<>(INITIAL_ICON_CACHE_CAPACITY);
148         } else {
149             // Use a dummy cache
150             mCache = new AbstractMap<ComponentKey, CacheEntry>() {
151                 @Override
152                 public Set<Entry<ComponentKey, CacheEntry>> entrySet() {
153                     return Collections.emptySet();
154                 }
155 
156                 @Override
157                 public CacheEntry put(ComponentKey key, CacheEntry value) {
158                     return value;
159                 }
160             };
161         }
162 
163         updateSystemState();
164         mIconDpi = iconDpi;
165         mIconDb = new IconDB(context, dbFileName, iconPixelSize);
166     }
167 
168     /**
169      * Returns the persistable serial number for {@param user}. Subclass should implement proper
170      * caching strategy to avoid making binder call every time.
171      */
getSerialNumberForUser(@onNull final UserHandle user)172     protected abstract long getSerialNumberForUser(@NonNull final UserHandle user);
173 
174     /**
175      * Return true if the given app is an instant app and should be badged appropriately.
176      */
isInstantApp(@onNull final ApplicationInfo info)177     protected abstract boolean isInstantApp(@NonNull final ApplicationInfo info);
178 
179     /**
180      * Opens and returns an icon factory. The factory is recycled by the caller.
181      */
182     @NonNull
getIconFactory()183     public abstract BaseIconFactory getIconFactory();
184 
updateIconParams(final int iconDpi, final int iconPixelSize)185     public void updateIconParams(final int iconDpi, final int iconPixelSize) {
186         mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize));
187     }
188 
updateIconParamsBg(final int iconDpi, final int iconPixelSize)189     private synchronized void updateIconParamsBg(final int iconDpi, final int iconPixelSize) {
190         mIconDpi = iconDpi;
191         mDefaultIcon = null;
192         mUserFlagOpMap.clear();
193         mIconDb.clear();
194         mIconDb.close();
195         mIconDb = new IconDB(mContext, mDbFileName, iconPixelSize);
196         mCache.clear();
197     }
198 
199     @Nullable
getFullResIcon(@ullable final Resources resources, final int iconId)200     private Drawable getFullResIcon(@Nullable final Resources resources, final int iconId) {
201         if (resources != null && iconId != 0) {
202             try {
203                 return resources.getDrawableForDensity(iconId, mIconDpi);
204             } catch (Resources.NotFoundException e) {
205             }
206         }
207         return getFullResDefaultActivityIcon(mIconDpi);
208     }
209 
210     @Nullable
getFullResIcon(@onNull final String packageName, final int iconId)211     public Drawable getFullResIcon(@NonNull final String packageName, final int iconId) {
212         try {
213             return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId);
214         } catch (PackageManager.NameNotFoundException e) {
215         }
216         return getFullResDefaultActivityIcon(mIconDpi);
217     }
218 
219     @Nullable
getFullResIcon(@onNull final ActivityInfo info)220     public Drawable getFullResIcon(@NonNull final ActivityInfo info) {
221         try {
222             return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo),
223                     info.getIconResource());
224         } catch (PackageManager.NameNotFoundException e) {
225         }
226         return getFullResDefaultActivityIcon(mIconDpi);
227     }
228 
setIconUpdateInProgress(boolean updating)229     public void setIconUpdateInProgress(boolean updating) {
230         mIconUpdateInProgress = updating;
231     }
232 
isIconUpdateInProgress()233     public boolean isIconUpdateInProgress() {
234         return mIconUpdateInProgress;
235     }
236 
237     /**
238      * Remove any records for the supplied ComponentName.
239      */
remove(@onNull final ComponentName componentName, @NonNull final UserHandle user)240     public synchronized void remove(@NonNull final ComponentName componentName,
241             @NonNull final UserHandle user) {
242         mCache.remove(new ComponentKey(componentName, user));
243     }
244 
245     /**
246      * Remove any records for the supplied package name from memory.
247      */
removeFromMemCacheLocked(@ullable final String packageName, @Nullable final UserHandle user)248     private void removeFromMemCacheLocked(@Nullable final String packageName,
249             @Nullable final UserHandle user) {
250         HashSet<ComponentKey> forDeletion = new HashSet<>();
251         for (ComponentKey key : mCache.keySet()) {
252             if (key.componentName.getPackageName().equals(packageName)
253                     && key.user.equals(user)) {
254                 forDeletion.add(key);
255             }
256         }
257         for (ComponentKey condemned : forDeletion) {
258             mCache.remove(condemned);
259         }
260     }
261 
262     /**
263      * Removes the entries related to the given package in memory and persistent DB.
264      */
removeIconsForPkg(@onNull final String packageName, @NonNull final UserHandle user)265     public synchronized void removeIconsForPkg(@NonNull final String packageName,
266             @NonNull final UserHandle user) {
267         removeFromMemCacheLocked(packageName, user);
268         long userSerial = getSerialNumberForUser(user);
269         mIconDb.delete(
270                 IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?",
271                 new String[]{packageName + "/%", Long.toString(userSerial)});
272     }
273 
274     @NonNull
getUpdateHandler()275     public IconCacheUpdateHandler getUpdateHandler() {
276         updateSystemState();
277         return new IconCacheUpdateHandler(this);
278     }
279 
280     /**
281      * Refreshes the system state definition used to check the validity of the cache. It
282      * incorporates all the properties that can affect the cache like the list of enabled locale
283      * and system-version.
284      */
updateSystemState()285     private void updateSystemState() {
286         mLocaleList = mContext.getResources().getConfiguration().getLocales();
287         mSystemState = mLocaleList.toLanguageTags() + "," + Build.VERSION.SDK_INT;
288         mUserFormatString.clear();
289     }
290 
291     @NonNull
getIconSystemState(@ullable final String packageName)292     protected String getIconSystemState(@Nullable final String packageName) {
293         return mSystemState;
294     }
295 
getUserBadgedLabel(CharSequence label, UserHandle user)296     public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) {
297         int key = user.hashCode();
298         int index = mUserFormatString.indexOfKey(key);
299         String format;
300         if (index < 0) {
301             format = mPackageManager.getUserBadgedLabel(IDENTITY_FORMAT_STRING, user).toString();
302             if (TextUtils.equals(IDENTITY_FORMAT_STRING, format)) {
303                 format = null;
304             }
305             mUserFormatString.put(key, format);
306         } else {
307             format = mUserFormatString.valueAt(index);
308         }
309         return format == null ? label : String.format(format, label);
310     }
311 
312     /**
313      * Adds an entry into the DB and the in-memory cache.
314      *
315      * @param replaceExisting if true, it will recreate the bitmap even if it already exists in
316      *                        the memory. This is useful then the previous bitmap was created using
317      *                        old data.
318      */
319     @VisibleForTesting
addIconToDBAndMemCache(@onNull final T object, @NonNull final CachingLogic<T> cachingLogic, @NonNull final PackageInfo info, final long userSerial, final boolean replaceExisting)320     public synchronized <T> void addIconToDBAndMemCache(@NonNull final T object,
321             @NonNull final CachingLogic<T> cachingLogic, @NonNull final PackageInfo info,
322             final long userSerial, final boolean replaceExisting) {
323         UserHandle user = cachingLogic.getUser(object);
324         ComponentName componentName = cachingLogic.getComponent(object);
325 
326         final ComponentKey key = new ComponentKey(componentName, user);
327         CacheEntry entry = null;
328         if (!replaceExisting) {
329             entry = mCache.get(key);
330             // We can't reuse the entry if the high-res icon is not present.
331             if (entry == null || entry.bitmap.isNullOrLowRes()) {
332                 entry = null;
333             }
334         }
335         if (entry == null) {
336             entry = new CacheEntry();
337             entry.bitmap = cachingLogic.loadIcon(mContext, object);
338         }
339         // Icon can't be loaded from cachingLogic, which implies alternative icon was loaded
340         // (e.g. fallback icon, default icon). So we drop here since there's no point in caching
341         // an empty entry.
342         if (entry.bitmap.isNullOrLowRes()) return;
343 
344         CharSequence entryTitle = cachingLogic.getLabel(object);
345         if (TextUtils.isEmpty(entryTitle)) {
346             if (entryTitle == null) {
347                 Log.wtf(TAG, "No label returned from caching logic instance: " + cachingLogic);
348             }
349             entryTitle = componentName.getPackageName();
350         }
351         entry.title = entryTitle;
352 
353         entry.contentDescription = getUserBadgedLabel(entry.title, user);
354         if (cachingLogic.addToMemCache()) mCache.put(key, entry);
355 
356         ContentValues values = newContentValues(entry.bitmap, entry.title.toString(),
357                 componentName.getPackageName(), cachingLogic.getKeywords(object, mLocaleList));
358         addIconToDB(values, componentName, info, userSerial,
359                 cachingLogic.getLastUpdatedTime(object, info));
360     }
361 
362     /**
363      * Updates {@param values} to contain versioning information and adds it to the DB.
364      *
365      * @param values {@link ContentValues} containing icon & title
366      */
addIconToDB(@onNull final ContentValues values, @NonNull final ComponentName key, @NonNull final PackageInfo info, final long userSerial, final long lastUpdateTime)367     private void addIconToDB(@NonNull final ContentValues values, @NonNull final ComponentName key,
368             @NonNull final PackageInfo info, final long userSerial, final long lastUpdateTime) {
369         values.put(IconDB.COLUMN_COMPONENT, key.flattenToString());
370         values.put(IconDB.COLUMN_USER, userSerial);
371         values.put(IconDB.COLUMN_LAST_UPDATED, lastUpdateTime);
372         values.put(IconDB.COLUMN_VERSION, info.versionCode);
373         mIconDb.insertOrReplace(values);
374     }
375 
376     @NonNull
getDefaultIcon(@onNull final UserHandle user)377     public synchronized BitmapInfo getDefaultIcon(@NonNull final UserHandle user) {
378         if (mDefaultIcon == null) {
379             try (BaseIconFactory li = getIconFactory()) {
380                 mDefaultIcon = li.makeDefaultIcon();
381             }
382         }
383         return mDefaultIcon.withFlags(getUserFlagOpLocked(user));
384     }
385 
386     @NonNull
getUserFlagOpLocked(@onNull final UserHandle user)387     protected FlagOp getUserFlagOpLocked(@NonNull final UserHandle user) {
388         int key = user.hashCode();
389         int index;
390         if ((index = mUserFlagOpMap.indexOfKey(key)) >= 0) {
391             return mUserFlagOpMap.valueAt(index);
392         } else {
393             try (BaseIconFactory li = getIconFactory()) {
394                 FlagOp op = li.getBitmapFlagOp(new IconOptions().setUser(user));
395                 mUserFlagOpMap.put(key, op);
396                 return op;
397             }
398         }
399     }
400 
isDefaultIcon(@onNull final BitmapInfo icon, @NonNull final UserHandle user)401     public boolean isDefaultIcon(@NonNull final BitmapInfo icon, @NonNull final UserHandle user) {
402         return getDefaultIcon(user).icon == icon.icon;
403     }
404 
405     /**
406      * Retrieves the entry from the cache. If the entry is not present, it creates a new entry.
407      * This method is not thread safe, it must be called from a synchronized method.
408      */
409     @NonNull
cacheLocked( @onNull final ComponentName componentName, @NonNull final UserHandle user, @NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic, final boolean usePackageIcon, final boolean useLowResIcon)410     protected <T> CacheEntry cacheLocked(
411             @NonNull final ComponentName componentName, @NonNull final UserHandle user,
412             @NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic,
413             final boolean usePackageIcon, final boolean useLowResIcon) {
414         return cacheLocked(
415                 componentName,
416                 user,
417                 infoProvider,
418                 cachingLogic,
419                 null,
420                 usePackageIcon,
421                 useLowResIcon);
422     }
423 
424     @NonNull
cacheLocked( @onNull final ComponentName componentName, @NonNull final UserHandle user, @NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic, @Nullable final Cursor cursor, final boolean usePackageIcon, final boolean useLowResIcon)425     protected <T> CacheEntry cacheLocked(
426             @NonNull final ComponentName componentName, @NonNull final UserHandle user,
427             @NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic,
428             @Nullable final Cursor cursor, final boolean usePackageIcon,
429             final boolean useLowResIcon) {
430         assertWorkerThread();
431         ComponentKey cacheKey = new ComponentKey(componentName, user);
432         CacheEntry entry = mCache.get(cacheKey);
433         if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) {
434             entry = new CacheEntry();
435             if (cachingLogic.addToMemCache()) {
436                 mCache.put(cacheKey, entry);
437             }
438 
439             // Check the DB first.
440             T object = null;
441             boolean providerFetchedOnce = false;
442             boolean cacheEntryUpdated = cursor == null
443                     ? getEntryFromDBLocked(cacheKey, entry, useLowResIcon)
444                     : updateTitleAndIconLocked(cacheKey, entry, cursor, useLowResIcon);
445             if (!cacheEntryUpdated) {
446                 object = infoProvider.get();
447                 providerFetchedOnce = true;
448 
449                 loadFallbackIcon(
450                         object,
451                         entry,
452                         cachingLogic,
453                         usePackageIcon,
454                         /* usePackageTitle= */ true,
455                         componentName,
456                         user);
457             }
458 
459             if (TextUtils.isEmpty(entry.title)) {
460                 if (object == null && !providerFetchedOnce) {
461                     object = infoProvider.get();
462                     providerFetchedOnce = true;
463                 }
464                 if (object != null) {
465                     loadFallbackTitle(object, entry, cachingLogic, user);
466                 }
467             }
468         }
469         return entry;
470     }
471 
472     /**
473      * Fallback method for loading an icon bitmap.
474      */
loadFallbackIcon(@ullable final T object, @NonNull final CacheEntry entry, @NonNull final CachingLogic<T> cachingLogic, final boolean usePackageIcon, final boolean usePackageTitle, @NonNull final ComponentName componentName, @NonNull final UserHandle user)475     protected <T> void loadFallbackIcon(@Nullable final T object, @NonNull final CacheEntry entry,
476             @NonNull final CachingLogic<T> cachingLogic, final boolean usePackageIcon,
477             final boolean usePackageTitle, @NonNull final ComponentName componentName,
478             @NonNull final UserHandle user) {
479         if (object != null) {
480             entry.bitmap = cachingLogic.loadIcon(mContext, object);
481         } else {
482             if (usePackageIcon) {
483                 CacheEntry packageEntry = getEntryForPackageLocked(
484                         componentName.getPackageName(), user, false);
485                 if (DEBUG) {
486                     Log.d(TAG, "using package default icon for "
487                             + componentName.toShortString());
488                 }
489                 entry.bitmap = packageEntry.bitmap;
490                 entry.contentDescription = packageEntry.contentDescription;
491 
492                 if (usePackageTitle) {
493                     entry.title = packageEntry.title;
494                 }
495             }
496             if (entry.bitmap == null) {
497                 // TODO: entry.bitmap can never be null, so this should not happen at all.
498                 Log.wtf(TAG, "using default icon for " + componentName.toShortString());
499                 entry.bitmap = getDefaultIcon(user);
500             }
501         }
502     }
503 
504     /**
505      * Fallback method for loading an app title.
506      */
loadFallbackTitle( @onNull final T object, @NonNull final CacheEntry entry, @NonNull final CachingLogic<T> cachingLogic, @NonNull final UserHandle user)507     protected <T> void loadFallbackTitle(
508             @NonNull final T object, @NonNull final CacheEntry entry,
509             @NonNull final CachingLogic<T> cachingLogic, @NonNull final UserHandle user) {
510         entry.title = cachingLogic.getLabel(object);
511         if (TextUtils.isEmpty(entry.title)) {
512             entry.title = cachingLogic.getComponent(object).getPackageName();
513         }
514         entry.contentDescription = getUserBadgedLabel(
515                 cachingLogic.getDescription(object, entry.title), user);
516     }
517 
clearMemoryCache()518     public synchronized void clearMemoryCache() {
519         assertWorkerThread();
520         mCache.clear();
521     }
522 
523     /**
524      * Adds a default package entry in the cache. This entry is not persisted and will be removed
525      * when the cache is flushed.
526      */
cachePackageInstallInfo(@onNull final String packageName, @NonNull final UserHandle user, @Nullable final Bitmap icon, @Nullable final CharSequence title)527     protected synchronized void cachePackageInstallInfo(@NonNull final String packageName,
528             @NonNull final UserHandle user, @Nullable final Bitmap icon,
529             @Nullable final CharSequence title) {
530         removeFromMemCacheLocked(packageName, user);
531 
532         ComponentKey cacheKey = getPackageKey(packageName, user);
533         CacheEntry entry = mCache.get(cacheKey);
534 
535         // For icon caching, do not go through DB. Just update the in-memory entry.
536         if (entry == null) {
537             entry = new CacheEntry();
538         }
539         if (!TextUtils.isEmpty(title)) {
540             entry.title = title;
541         }
542         if (icon != null) {
543             BaseIconFactory li = getIconFactory();
544             entry.bitmap = li.createShapedIconBitmap(icon, new IconOptions().setUser(user));
545             li.close();
546         }
547         if (!TextUtils.isEmpty(title) && entry.bitmap.icon != null) {
548             mCache.put(cacheKey, entry);
549         }
550     }
551 
552     @NonNull
getPackageKey(@onNull final String packageName, @NonNull final UserHandle user)553     private static ComponentKey getPackageKey(@NonNull final String packageName,
554             @NonNull final UserHandle user) {
555         ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME);
556         return new ComponentKey(cn, user);
557     }
558 
559     /**
560      * Gets an entry for the package, which can be used as a fallback entry for various components.
561      * This method is not thread safe, it must be called from a synchronized method.
562      */
563     @WorkerThread
564     @NonNull
565     @SuppressWarnings("NewApi")
getEntryForPackageLocked(@onNull final String packageName, @NonNull final UserHandle user, final boolean useLowResIcon)566     protected CacheEntry getEntryForPackageLocked(@NonNull final String packageName,
567             @NonNull final UserHandle user, final boolean useLowResIcon) {
568         assertWorkerThread();
569         ComponentKey cacheKey = getPackageKey(packageName, user);
570         CacheEntry entry = mCache.get(cacheKey);
571 
572         if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) {
573             entry = new CacheEntry();
574             boolean entryUpdated = true;
575 
576             // Check the DB first.
577             if (!getEntryFromDBLocked(cacheKey, entry, useLowResIcon)) {
578                 try {
579                     long flags = Process.myUserHandle().equals(user) ? 0 :
580                             PackageManager.GET_UNINSTALLED_PACKAGES;
581                     flags |= PackageManager.MATCH_ARCHIVED_PACKAGES;
582                     PackageInfo info = mPackageManager.getPackageInfo(packageName,
583                             PackageManager.PackageInfoFlags.of(flags));
584                     ApplicationInfo appInfo = info.applicationInfo;
585                     if (appInfo == null) {
586                         NameNotFoundException e = new NameNotFoundException(
587                                 "ApplicationInfo is null");
588                         logdPersistently(TAG,
589                                 String.format("ApplicationInfo is null for %s", packageName),
590                                 e);
591                         throw e;
592                     }
593 
594                     BaseIconFactory li = getIconFactory();
595                     // Load the full res icon for the application, but if useLowResIcon is set, then
596                     // only keep the low resolution icon instead of the larger full-sized icon
597                     Drawable appIcon = appInfo.loadIcon(mPackageManager);
598                     if (mPackageManager.isDefaultApplicationIcon(appIcon)) {
599                         logdPersistently(TAG,
600                                 String.format("Default icon returned for %s", packageName),
601                                 null);
602                     }
603                     BitmapInfo iconInfo = li.createBadgedIconBitmap(appIcon,
604                             new IconOptions().setUser(user).setInstantApp(isInstantApp(appInfo)));
605                     li.close();
606 
607                     entry.title = appInfo.loadLabel(mPackageManager);
608                     entry.contentDescription = getUserBadgedLabel(entry.title, user);
609                     entry.bitmap = useLowResIcon
610                             ? BitmapInfo.of(LOW_RES_ICON, iconInfo.color)
611                             : iconInfo;
612 
613                     // Add the icon in the DB here, since these do not get written during
614                     // package updates.
615                     ContentValues values = newContentValues(
616                             iconInfo, entry.title.toString(), packageName, null);
617                     addIconToDB(values, cacheKey.componentName, info, getSerialNumberForUser(user),
618                             info.lastUpdateTime);
619 
620                 } catch (NameNotFoundException e) {
621                     if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
622                     entryUpdated = false;
623                 }
624             }
625 
626             // Only add a filled-out entry to the cache
627             if (entryUpdated) {
628                 mCache.put(cacheKey, entry);
629             }
630         }
631         return entry;
632     }
633 
getEntryFromDBLocked(@onNull final ComponentKey cacheKey, @NonNull final CacheEntry entry, final boolean lowRes)634     protected boolean getEntryFromDBLocked(@NonNull final ComponentKey cacheKey,
635             @NonNull final CacheEntry entry, final boolean lowRes) {
636         Cursor c = null;
637         Trace.beginSection("loadIconIndividually");
638         try {
639             c = mIconDb.query(
640                     lowRes ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES,
641                     IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
642                     new String[]{
643                             cacheKey.componentName.flattenToString(),
644                             Long.toString(getSerialNumberForUser(cacheKey.user))});
645             if (c.moveToNext()) {
646                 return updateTitleAndIconLocked(cacheKey, entry, c, lowRes);
647             }
648         } catch (SQLiteException e) {
649             Log.d(TAG, "Error reading icon cache", e);
650         } finally {
651             if (c != null) {
652                 c.close();
653             }
654             Trace.endSection();
655         }
656         return false;
657     }
658 
updateTitleAndIconLocked( @onNull final ComponentKey cacheKey, @NonNull final CacheEntry entry, @NonNull final Cursor c, final boolean lowRes)659     private boolean updateTitleAndIconLocked(
660             @NonNull final ComponentKey cacheKey, @NonNull final CacheEntry entry,
661             @NonNull final Cursor c, final boolean lowRes) {
662         // Set the alpha to be 255, so that we never have a wrong color
663         entry.bitmap = BitmapInfo.of(LOW_RES_ICON,
664                 setColorAlphaBound(c.getInt(IconDB.INDEX_COLOR), 255));
665         entry.title = c.getString(IconDB.INDEX_TITLE);
666         if (entry.title == null) {
667             entry.title = "";
668             entry.contentDescription = "";
669         } else {
670             entry.contentDescription = getUserBadgedLabel(entry.title, cacheKey.user);
671         }
672 
673         if (!lowRes) {
674             byte[] data = c.getBlob(IconDB.INDEX_ICON);
675             if (data == null) {
676                 return false;
677             }
678             try {
679                 BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
680                 decodeOptions.inPreferredConfig = Config.HARDWARE;
681                 entry.bitmap = BitmapInfo.of(
682                         requireNonNull(decodeByteArray(data, 0, data.length, decodeOptions)),
683                         entry.bitmap.color);
684             } catch (Exception e) {
685                 return false;
686             }
687 
688             // Decode mono bitmap
689             data = c.getBlob(IconDB.INDEX_MONO_ICON);
690             Bitmap icon = entry.bitmap.icon;
691             if (data != null && data.length == icon.getHeight() * icon.getWidth()) {
692                 Bitmap monoBitmap = Bitmap.createBitmap(
693                         icon.getWidth(), icon.getHeight(), Config.ALPHA_8);
694                 monoBitmap.copyPixelsFromBuffer(ByteBuffer.wrap(data));
695                 Bitmap hwMonoBitmap = monoBitmap.copy(Config.HARDWARE, false /*isMutable*/);
696                 if (hwMonoBitmap != null) {
697                     monoBitmap.recycle();
698                     monoBitmap = hwMonoBitmap;
699                 }
700                 try (BaseIconFactory factory = getIconFactory()) {
701                     entry.bitmap.setMonoIcon(monoBitmap, factory);
702                 }
703             }
704         }
705         entry.bitmap.flags = c.getInt(IconDB.INDEX_FLAGS);
706         entry.bitmap = entry.bitmap.withFlags(getUserFlagOpLocked(cacheKey.user));
707         return entry.bitmap != null;
708     }
709 
710     /**
711      * Returns a cursor for an arbitrary query to the cache db
712      */
queryCacheDb(String[] columns, String selection, String[] selectionArgs)713     public synchronized Cursor queryCacheDb(String[] columns, String selection,
714             String[] selectionArgs) {
715         return mIconDb.query(columns, selection, selectionArgs);
716     }
717 
718     /**
719      * Cache class to store the actual entries on disk
720      */
721     public static final class IconDB extends SQLiteCacheHelper {
722         private static final int RELEASE_VERSION = 34;
723 
724         public static final String TABLE_NAME = "icons";
725         public static final String COLUMN_ROWID = "rowid";
726         public static final String COLUMN_COMPONENT = "componentName";
727         public static final String COLUMN_USER = "profileId";
728         public static final String COLUMN_LAST_UPDATED = "lastUpdated";
729         public static final String COLUMN_VERSION = "version";
730         public static final String COLUMN_ICON = "icon";
731         public static final String COLUMN_ICON_COLOR = "icon_color";
732         public static final String COLUMN_MONO_ICON = "mono_icon";
733         public static final String COLUMN_FLAGS = "flags";
734         public static final String COLUMN_LABEL = "label";
735         public static final String COLUMN_SYSTEM_STATE = "system_state";
736         public static final String COLUMN_KEYWORDS = "keywords";
737 
738         public static final String[] COLUMNS_LOW_RES = new String[]{
739                 COLUMN_COMPONENT,
740                 COLUMN_LABEL,
741                 COLUMN_ICON_COLOR,
742                 COLUMN_FLAGS};
743         public static final String[] COLUMNS_HIGH_RES = Arrays.copyOf(COLUMNS_LOW_RES,
744                 COLUMNS_LOW_RES.length + 2, String[].class);
745 
746         static {
747             COLUMNS_HIGH_RES[COLUMNS_LOW_RES.length] = COLUMN_ICON;
748             COLUMNS_HIGH_RES[COLUMNS_LOW_RES.length + 1] = COLUMN_MONO_ICON;
749         }
750 
751         private static final int INDEX_TITLE = Arrays.asList(COLUMNS_LOW_RES).indexOf(COLUMN_LABEL);
752         private static final int INDEX_COLOR = Arrays.asList(COLUMNS_LOW_RES)
753                 .indexOf(COLUMN_ICON_COLOR);
754         private static final int INDEX_FLAGS = Arrays.asList(COLUMNS_LOW_RES).indexOf(COLUMN_FLAGS);
755         private static final int INDEX_ICON = COLUMNS_LOW_RES.length;
756         private static final int INDEX_MONO_ICON = INDEX_ICON + 1;
757 
IconDB(Context context, String dbFileName, int iconPixelSize)758         public IconDB(Context context, String dbFileName, int iconPixelSize) {
759             super(context, dbFileName, (RELEASE_VERSION << 16) + iconPixelSize, TABLE_NAME);
760         }
761 
762         @Override
onCreateTable(SQLiteDatabase db)763         protected void onCreateTable(SQLiteDatabase db) {
764             db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ("
765                     + COLUMN_COMPONENT + " TEXT NOT NULL, "
766                     + COLUMN_USER + " INTEGER NOT NULL, "
767                     + COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, "
768                     + COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, "
769                     + COLUMN_ICON + " BLOB, "
770                     + COLUMN_MONO_ICON + " BLOB, "
771                     + COLUMN_ICON_COLOR + " INTEGER NOT NULL DEFAULT 0, "
772                     + COLUMN_FLAGS + " INTEGER NOT NULL DEFAULT 0, "
773                     + COLUMN_LABEL + " TEXT, "
774                     + COLUMN_SYSTEM_STATE + " TEXT, "
775                     + COLUMN_KEYWORDS + " TEXT, "
776                     + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") "
777                     + ");");
778         }
779     }
780 
781     @NonNull
newContentValues(@onNull final BitmapInfo bitmapInfo, @NonNull final String label, @NonNull final String packageName, @Nullable final String keywords)782     private ContentValues newContentValues(@NonNull final BitmapInfo bitmapInfo,
783             @NonNull final String label, @NonNull final String packageName,
784             @Nullable final String keywords) {
785         ContentValues values = new ContentValues();
786         if (bitmapInfo.canPersist()) {
787             values.put(IconDB.COLUMN_ICON, flattenBitmap(bitmapInfo.icon));
788 
789             // Persist mono bitmap as alpha channel
790             Bitmap mono = bitmapInfo.getMono();
791             if (mono != null && mono.getHeight() == bitmapInfo.icon.getHeight()
792                     && mono.getWidth() == bitmapInfo.icon.getWidth()
793                     && mono.getConfig() == Config.ALPHA_8) {
794                 byte[] pixels = new byte[mono.getWidth() * mono.getHeight()];
795                 mono.copyPixelsToBuffer(ByteBuffer.wrap(pixels));
796                 values.put(IconDB.COLUMN_MONO_ICON, pixels);
797             } else {
798                 values.put(IconDB.COLUMN_MONO_ICON, (byte[]) null);
799             }
800         } else {
801             values.put(IconDB.COLUMN_ICON, (byte[]) null);
802             values.put(IconDB.COLUMN_MONO_ICON, (byte[]) null);
803         }
804         values.put(IconDB.COLUMN_ICON_COLOR, bitmapInfo.color);
805         values.put(IconDB.COLUMN_FLAGS, bitmapInfo.flags);
806 
807         values.put(IconDB.COLUMN_LABEL, label);
808         values.put(IconDB.COLUMN_SYSTEM_STATE, getIconSystemState(packageName));
809         values.put(IconDB.COLUMN_KEYWORDS, keywords);
810         return values;
811     }
812 
assertWorkerThread()813     private void assertWorkerThread() {
814         if (Looper.myLooper() != mBgLooper) {
815             throw new IllegalStateException("Cache accessed on wrong thread " + Looper.myLooper());
816         }
817     }
818 
819     /** Log to Log.d. Subclasses can override this method to log persistently for debugging. */
logdPersistently(String tag, String message, @Nullable Exception e)820     protected void logdPersistently(String tag, String message, @Nullable Exception e) {
821         Log.d(tag, message, e);
822     }
823 }
824