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