• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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 
17 package com.android.launcher3;
18 
19 import android.content.ComponentName;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.ActivityInfo;
24 import android.content.pm.ApplicationInfo;
25 import android.content.pm.LauncherActivityInfo;
26 import android.content.pm.PackageInfo;
27 import android.content.pm.PackageManager;
28 import android.content.pm.PackageManager.NameNotFoundException;
29 import android.content.res.Resources;
30 import android.database.Cursor;
31 import android.database.sqlite.SQLiteDatabase;
32 import android.database.sqlite.SQLiteException;
33 import android.graphics.Bitmap;
34 import android.graphics.BitmapFactory;
35 import android.graphics.drawable.Drawable;
36 import android.os.Build;
37 import android.os.Handler;
38 import android.os.Process;
39 import android.os.SystemClock;
40 import android.os.UserHandle;
41 import android.support.annotation.NonNull;
42 import android.text.TextUtils;
43 import android.util.Log;
44 import com.android.launcher3.compat.LauncherAppsCompat;
45 import com.android.launcher3.compat.UserManagerCompat;
46 import com.android.launcher3.config.FeatureFlags;
47 import com.android.launcher3.graphics.LauncherIcons;
48 import com.android.launcher3.model.PackageItemInfo;
49 import com.android.launcher3.util.ComponentKey;
50 import com.android.launcher3.util.InstantAppResolver;
51 import com.android.launcher3.util.Preconditions;
52 import com.android.launcher3.util.Provider;
53 import com.android.launcher3.util.SQLiteCacheHelper;
54 import com.android.launcher3.util.Thunk;
55 import java.util.Collections;
56 import java.util.HashMap;
57 import java.util.HashSet;
58 import java.util.List;
59 import java.util.Set;
60 import java.util.Stack;
61 
62 /**
63  * Cache of application icons.  Icons can be made from any thread.
64  */
65 public class IconCache {
66 
67     private static final String TAG = "Launcher.IconCache";
68 
69     private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
70 
71     // Empty class name is used for storing package default entry.
72     public static final String EMPTY_CLASS_NAME = ".";
73 
74     private static final boolean DEBUG = false;
75     private static final boolean DEBUG_IGNORE_CACHE = false;
76 
77     private static final int LOW_RES_SCALE_FACTOR = 5;
78 
79     @Thunk static final Object ICON_UPDATE_TOKEN = new Object();
80 
81     public static class CacheEntry {
82         public Bitmap icon;
83         public CharSequence title = "";
84         public CharSequence contentDescription = "";
85         public boolean isLowResIcon;
86     }
87 
88     private final HashMap<UserHandle, Bitmap> mDefaultIcons = new HashMap<>();
89     @Thunk final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
90 
91     private final Context mContext;
92     private final PackageManager mPackageManager;
93     private final IconProvider mIconProvider;
94     @Thunk final UserManagerCompat mUserManager;
95     private final LauncherAppsCompat mLauncherApps;
96     private final HashMap<ComponentKey, CacheEntry> mCache =
97             new HashMap<>(INITIAL_ICON_CACHE_CAPACITY);
98     private final InstantAppResolver mInstantAppResolver;
99     private final int mIconDpi;
100     @Thunk final IconDB mIconDb;
101 
102     @Thunk final Handler mWorkerHandler;
103 
104     private final BitmapFactory.Options mLowResOptions;
105 
IconCache(Context context, InvariantDeviceProfile inv)106     public IconCache(Context context, InvariantDeviceProfile inv) {
107         mContext = context;
108         mPackageManager = context.getPackageManager();
109         mUserManager = UserManagerCompat.getInstance(mContext);
110         mLauncherApps = LauncherAppsCompat.getInstance(mContext);
111         mInstantAppResolver = InstantAppResolver.newInstance(mContext);
112         mIconDpi = inv.fillResIconDpi;
113         mIconDb = new IconDB(context, inv.iconBitmapSize);
114 
115         mIconProvider = Utilities.getOverrideObject(
116                 IconProvider.class, context, R.string.icon_provider_class);
117         mWorkerHandler = new Handler(LauncherModel.getWorkerLooper());
118 
119         mLowResOptions = new BitmapFactory.Options();
120         // Always prefer RGB_565 config for low res. If the bitmap has transparency, it will
121         // automatically be loaded as ALPHA_8888.
122         mLowResOptions.inPreferredConfig = Bitmap.Config.RGB_565;
123     }
124 
getFullResDefaultActivityIcon()125     private Drawable getFullResDefaultActivityIcon() {
126         return getFullResIcon(Resources.getSystem(), Utilities.ATLEAST_OREO ?
127                 android.R.drawable.sym_def_app_icon : android.R.mipmap.sym_def_app_icon);
128     }
129 
getFullResIcon(Resources resources, int iconId)130     private Drawable getFullResIcon(Resources resources, int iconId) {
131         Drawable d;
132         try {
133             d = resources.getDrawableForDensity(iconId, mIconDpi);
134         } catch (Resources.NotFoundException e) {
135             d = null;
136         }
137 
138         return (d != null) ? d : getFullResDefaultActivityIcon();
139     }
140 
getFullResIcon(String packageName, int iconId)141     public Drawable getFullResIcon(String packageName, int iconId) {
142         Resources resources;
143         try {
144             resources = mPackageManager.getResourcesForApplication(packageName);
145         } catch (PackageManager.NameNotFoundException e) {
146             resources = null;
147         }
148         if (resources != null) {
149             if (iconId != 0) {
150                 return getFullResIcon(resources, iconId);
151             }
152         }
153         return getFullResDefaultActivityIcon();
154     }
155 
getFullResIcon(ActivityInfo info)156     public Drawable getFullResIcon(ActivityInfo info) {
157         Resources resources;
158         try {
159             resources = mPackageManager.getResourcesForApplication(
160                     info.applicationInfo);
161         } catch (PackageManager.NameNotFoundException e) {
162             resources = null;
163         }
164         if (resources != null) {
165             int iconId = info.getIconResource();
166             if (iconId != 0) {
167                 return getFullResIcon(resources, iconId);
168             }
169         }
170 
171         return getFullResDefaultActivityIcon();
172     }
173 
getFullResIcon(LauncherActivityInfo info)174     public Drawable getFullResIcon(LauncherActivityInfo info) {
175         return getFullResIcon(info, true);
176     }
177 
getFullResIcon(LauncherActivityInfo info, boolean flattenDrawable)178     public Drawable getFullResIcon(LauncherActivityInfo info, boolean flattenDrawable) {
179         return mIconProvider.getIcon(info, mIconDpi, flattenDrawable);
180     }
181 
makeDefaultIcon(UserHandle user)182     protected Bitmap makeDefaultIcon(UserHandle user) {
183         Drawable unbadged = getFullResDefaultActivityIcon();
184         return LauncherIcons.createBadgedIconBitmap(unbadged, user, mContext, Build.VERSION_CODES.O);
185     }
186 
187     /**
188      * Remove any records for the supplied ComponentName.
189      */
remove(ComponentName componentName, UserHandle user)190     public synchronized void remove(ComponentName componentName, UserHandle user) {
191         mCache.remove(new ComponentKey(componentName, user));
192     }
193 
194     /**
195      * Remove any records for the supplied package name from memory.
196      */
removeFromMemCacheLocked(String packageName, UserHandle user)197     private void removeFromMemCacheLocked(String packageName, UserHandle user) {
198         HashSet<ComponentKey> forDeletion = new HashSet<>();
199         for (ComponentKey key: mCache.keySet()) {
200             if (key.componentName.getPackageName().equals(packageName)
201                     && key.user.equals(user)) {
202                 forDeletion.add(key);
203             }
204         }
205         for (ComponentKey condemned: forDeletion) {
206             mCache.remove(condemned);
207         }
208     }
209 
210     /**
211      * Updates the entries related to the given package in memory and persistent DB.
212      */
updateIconsForPkg(String packageName, UserHandle user)213     public synchronized void updateIconsForPkg(String packageName, UserHandle user) {
214         removeIconsForPkg(packageName, user);
215         try {
216             PackageInfo info = mPackageManager.getPackageInfo(packageName,
217                     PackageManager.GET_UNINSTALLED_PACKAGES);
218             long userSerial = mUserManager.getSerialNumberForUser(user);
219             for (LauncherActivityInfo app : mLauncherApps.getActivityList(packageName, user)) {
220                 addIconToDBAndMemCache(app, info, userSerial, false /*replace existing*/);
221             }
222         } catch (NameNotFoundException e) {
223             Log.d(TAG, "Package not found", e);
224         }
225     }
226 
227     /**
228      * Removes the entries related to the given package in memory and persistent DB.
229      */
removeIconsForPkg(String packageName, UserHandle user)230     public synchronized void removeIconsForPkg(String packageName, UserHandle user) {
231         removeFromMemCacheLocked(packageName, user);
232         long userSerial = mUserManager.getSerialNumberForUser(user);
233         mIconDb.delete(
234                 IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?",
235                 new String[]{packageName + "/%", Long.toString(userSerial)});
236     }
237 
updateDbIcons(Set<String> ignorePackagesForMainUser)238     public void updateDbIcons(Set<String> ignorePackagesForMainUser) {
239         // Remove all active icon update tasks.
240         mWorkerHandler.removeCallbacksAndMessages(ICON_UPDATE_TOKEN);
241 
242         mIconProvider.updateSystemStateString();
243         for (UserHandle user : mUserManager.getUserProfiles()) {
244             // Query for the set of apps
245             final List<LauncherActivityInfo> apps = mLauncherApps.getActivityList(null, user);
246             // Fail if we don't have any apps
247             // TODO: Fix this. Only fail for the current user.
248             if (apps == null || apps.isEmpty()) {
249                 return;
250             }
251 
252             // Update icon cache. This happens in segments and {@link #onPackageIconsUpdated}
253             // is called by the icon cache when the job is complete.
254             updateDBIcons(user, apps, Process.myUserHandle().equals(user)
255                     ? ignorePackagesForMainUser : Collections.<String>emptySet());
256         }
257     }
258 
259     /**
260      * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
261      * the DB and are updated.
262      * @return The set of packages for which icons have updated.
263      */
updateDBIcons(UserHandle user, List<LauncherActivityInfo> apps, Set<String> ignorePackages)264     private void updateDBIcons(UserHandle user, List<LauncherActivityInfo> apps,
265             Set<String> ignorePackages) {
266         long userSerial = mUserManager.getSerialNumberForUser(user);
267         PackageManager pm = mContext.getPackageManager();
268         HashMap<String, PackageInfo> pkgInfoMap = new HashMap<>();
269         for (PackageInfo info : pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) {
270             pkgInfoMap.put(info.packageName, info);
271         }
272 
273         HashMap<ComponentName, LauncherActivityInfo> componentMap = new HashMap<>();
274         for (LauncherActivityInfo app : apps) {
275             componentMap.put(app.getComponentName(), app);
276         }
277 
278         HashSet<Integer> itemsToRemove = new HashSet<>();
279         Stack<LauncherActivityInfo> appsToUpdate = new Stack<>();
280 
281         Cursor c = null;
282         try {
283             c = mIconDb.query(
284                     new String[]{IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT,
285                             IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION,
286                             IconDB.COLUMN_SYSTEM_STATE},
287                     IconDB.COLUMN_USER + " = ? ",
288                     new String[]{Long.toString(userSerial)});
289 
290             final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT);
291             final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED);
292             final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION);
293             final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID);
294             final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE);
295 
296             while (c.moveToNext()) {
297                 String cn = c.getString(indexComponent);
298                 ComponentName component = ComponentName.unflattenFromString(cn);
299                 PackageInfo info = pkgInfoMap.get(component.getPackageName());
300                 if (info == null) {
301                     if (!ignorePackages.contains(component.getPackageName())) {
302                         remove(component, user);
303                         itemsToRemove.add(c.getInt(rowIndex));
304                     }
305                     continue;
306                 }
307                 if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) {
308                     // Application is not present
309                     continue;
310                 }
311 
312                 long updateTime = c.getLong(indexLastUpdate);
313                 int version = c.getInt(indexVersion);
314                 LauncherActivityInfo app = componentMap.remove(component);
315                 if (version == info.versionCode && updateTime == info.lastUpdateTime &&
316                         TextUtils.equals(c.getString(systemStateIndex),
317                                 mIconProvider.getIconSystemState(info.packageName))) {
318                     continue;
319                 }
320                 if (app == null) {
321                     remove(component, user);
322                     itemsToRemove.add(c.getInt(rowIndex));
323                 } else {
324                     appsToUpdate.add(app);
325                 }
326             }
327         } catch (SQLiteException e) {
328             Log.d(TAG, "Error reading icon cache", e);
329             // Continue updating whatever we have read so far
330         } finally {
331             if (c != null) {
332                 c.close();
333             }
334         }
335         if (!itemsToRemove.isEmpty()) {
336             mIconDb.delete(
337                     Utilities.createDbSelectionQuery(IconDB.COLUMN_ROWID, itemsToRemove), null);
338         }
339 
340         // Insert remaining apps.
341         if (!componentMap.isEmpty() || !appsToUpdate.isEmpty()) {
342             Stack<LauncherActivityInfo> appsToAdd = new Stack<>();
343             appsToAdd.addAll(componentMap.values());
344             new SerializedIconUpdateTask(userSerial, pkgInfoMap,
345                     appsToAdd, appsToUpdate).scheduleNext();
346         }
347     }
348 
349     /**
350      * Adds an entry into the DB and the in-memory cache.
351      * @param replaceExisting if true, it will recreate the bitmap even if it already exists in
352      *                        the memory. This is useful then the previous bitmap was created using
353      *                        old data.
354      */
addIconToDBAndMemCache(LauncherActivityInfo app, PackageInfo info, long userSerial, boolean replaceExisting)355     @Thunk synchronized void addIconToDBAndMemCache(LauncherActivityInfo app,
356             PackageInfo info, long userSerial, boolean replaceExisting) {
357         final ComponentKey key = new ComponentKey(app.getComponentName(), app.getUser());
358         CacheEntry entry = null;
359         if (!replaceExisting) {
360             entry = mCache.get(key);
361             // We can't reuse the entry if the high-res icon is not present.
362             if (entry == null || entry.isLowResIcon || entry.icon == null) {
363                 entry = null;
364             }
365         }
366         if (entry == null) {
367             entry = new CacheEntry();
368             entry.icon = LauncherIcons.createBadgedIconBitmap(getFullResIcon(app), app.getUser(),
369                     mContext,  app.getApplicationInfo().targetSdkVersion);
370         }
371         entry.title = app.getLabel();
372         entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, app.getUser());
373         mCache.put(key, entry);
374 
375         Bitmap lowResIcon = generateLowResIcon(entry.icon);
376         ContentValues values = newContentValues(entry.icon, lowResIcon, entry.title.toString(),
377                 app.getApplicationInfo().packageName);
378         addIconToDB(values, app.getComponentName(), info, userSerial);
379     }
380 
381     /**
382      * Updates {@param values} to contain versioning information and adds it to the DB.
383      * @param values {@link ContentValues} containing icon & title
384      */
addIconToDB(ContentValues values, ComponentName key, PackageInfo info, long userSerial)385     private void addIconToDB(ContentValues values, ComponentName key,
386             PackageInfo info, long userSerial) {
387         values.put(IconDB.COLUMN_COMPONENT, key.flattenToString());
388         values.put(IconDB.COLUMN_USER, userSerial);
389         values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime);
390         values.put(IconDB.COLUMN_VERSION, info.versionCode);
391         mIconDb.insertOrReplace(values);
392     }
393 
394     /**
395      * Fetches high-res icon for the provided ItemInfo and updates the caller when done.
396      * @return a request ID that can be used to cancel the request.
397      */
updateIconInBackground(final ItemInfoUpdateReceiver caller, final ItemInfoWithIcon info)398     public IconLoadRequest updateIconInBackground(final ItemInfoUpdateReceiver caller,
399             final ItemInfoWithIcon info) {
400         Runnable request = new Runnable() {
401 
402             @Override
403             public void run() {
404                 if (info instanceof AppInfo || info instanceof ShortcutInfo) {
405                     getTitleAndIcon(info, false);
406                 } else if (info instanceof PackageItemInfo) {
407                     getTitleAndIconForApp((PackageItemInfo) info, false);
408                 }
409                 mMainThreadExecutor.execute(new Runnable() {
410 
411                     @Override
412                     public void run() {
413                         caller.reapplyItemInfo(info);
414                     }
415                 });
416             }
417         };
418         mWorkerHandler.post(request);
419         return new IconLoadRequest(request, mWorkerHandler);
420     }
421 
422     /**
423      * Updates {@param application} only if a valid entry is found.
424      */
updateTitleAndIcon(AppInfo application)425     public synchronized void updateTitleAndIcon(AppInfo application) {
426         CacheEntry entry = cacheLocked(application.componentName,
427                 Provider.<LauncherActivityInfo>of(null),
428                 application.user, false, application.usingLowResIcon);
429         if (entry.icon != null && !isDefaultIcon(entry.icon, application.user)) {
430             applyCacheEntry(entry, application);
431         }
432     }
433 
434     /**
435      * Fill in {@param info} with the icon and label for {@param activityInfo}
436      */
getTitleAndIcon(ItemInfoWithIcon info, LauncherActivityInfo activityInfo, boolean useLowResIcon)437     public synchronized void getTitleAndIcon(ItemInfoWithIcon info,
438             LauncherActivityInfo activityInfo, boolean useLowResIcon) {
439         // If we already have activity info, no need to use package icon
440         getTitleAndIcon(info, Provider.of(activityInfo), false, useLowResIcon);
441     }
442 
443     /**
444      * Fill in {@param info} with the icon and label. If the
445      * corresponding activity is not found, it reverts to the package icon.
446      */
getTitleAndIcon(ItemInfoWithIcon info, boolean useLowResIcon)447     public synchronized void getTitleAndIcon(ItemInfoWithIcon info, boolean useLowResIcon) {
448         // null info means not installed, but if we have a component from the intent then
449         // we should still look in the cache for restored app icons.
450         if (info.getTargetComponent() == null) {
451             info.iconBitmap = getDefaultIcon(info.user);
452             info.title = "";
453             info.contentDescription = "";
454             info.usingLowResIcon = false;
455         } else {
456             getTitleAndIcon(info, new ActivityInfoProvider(info.getIntent(), info.user),
457                     true, useLowResIcon);
458         }
459     }
460 
461     /**
462      * Fill in {@param shortcutInfo} with the icon and label for {@param info}
463      */
getTitleAndIcon( @onNull ItemInfoWithIcon infoInOut, @NonNull Provider<LauncherActivityInfo> activityInfoProvider, boolean usePkgIcon, boolean useLowResIcon)464     private synchronized void getTitleAndIcon(
465             @NonNull ItemInfoWithIcon infoInOut,
466             @NonNull Provider<LauncherActivityInfo> activityInfoProvider,
467             boolean usePkgIcon, boolean useLowResIcon) {
468         CacheEntry entry = cacheLocked(infoInOut.getTargetComponent(), activityInfoProvider,
469                 infoInOut.user, usePkgIcon, useLowResIcon);
470         applyCacheEntry(entry, infoInOut);
471     }
472 
473     /**
474      * Fill in {@param infoInOut} with the corresponding icon and label.
475      */
getTitleAndIconForApp( PackageItemInfo infoInOut, boolean useLowResIcon)476     public synchronized void getTitleAndIconForApp(
477             PackageItemInfo infoInOut, boolean useLowResIcon) {
478         CacheEntry entry = getEntryForPackageLocked(
479                 infoInOut.packageName, infoInOut.user, useLowResIcon);
480         applyCacheEntry(entry, infoInOut);
481     }
482 
applyCacheEntry(CacheEntry entry, ItemInfoWithIcon info)483     private void applyCacheEntry(CacheEntry entry, ItemInfoWithIcon info) {
484         info.title = Utilities.trim(entry.title);
485         info.contentDescription = entry.contentDescription;
486         info.iconBitmap = entry.icon == null ? getDefaultIcon(info.user) : entry.icon;
487         info.usingLowResIcon = entry.isLowResIcon;
488     }
489 
getDefaultIcon(UserHandle user)490     public synchronized Bitmap getDefaultIcon(UserHandle user) {
491         if (!mDefaultIcons.containsKey(user)) {
492             mDefaultIcons.put(user, makeDefaultIcon(user));
493         }
494         return mDefaultIcons.get(user);
495     }
496 
isDefaultIcon(Bitmap icon, UserHandle user)497     public boolean isDefaultIcon(Bitmap icon, UserHandle user) {
498         return mDefaultIcons.get(user) == icon;
499     }
500 
501     /**
502      * Retrieves the entry from the cache. If the entry is not present, it creates a new entry.
503      * This method is not thread safe, it must be called from a synchronized method.
504      */
cacheLocked( @onNull ComponentName componentName, @NonNull Provider<LauncherActivityInfo> infoProvider, UserHandle user, boolean usePackageIcon, boolean useLowResIcon)505     protected CacheEntry cacheLocked(
506             @NonNull ComponentName componentName,
507             @NonNull Provider<LauncherActivityInfo> infoProvider,
508             UserHandle user, boolean usePackageIcon, boolean useLowResIcon) {
509         Preconditions.assertWorkerThread();
510         ComponentKey cacheKey = new ComponentKey(componentName, user);
511         CacheEntry entry = mCache.get(cacheKey);
512         if (entry == null || (entry.isLowResIcon && !useLowResIcon)) {
513             entry = new CacheEntry();
514             mCache.put(cacheKey, entry);
515 
516             // Check the DB first.
517             LauncherActivityInfo info = null;
518             boolean providerFetchedOnce = false;
519 
520             if (!getEntryFromDB(cacheKey, entry, useLowResIcon) || DEBUG_IGNORE_CACHE) {
521                 info = infoProvider.get();
522                 providerFetchedOnce = true;
523 
524                 if (info != null) {
525                     entry.icon = LauncherIcons.createBadgedIconBitmap(
526                             getFullResIcon(info), info.getUser(), mContext,
527                             infoProvider.get().getApplicationInfo().targetSdkVersion);
528                 } else {
529                     if (usePackageIcon) {
530                         CacheEntry packageEntry = getEntryForPackageLocked(
531                                 componentName.getPackageName(), user, false);
532                         if (packageEntry != null) {
533                             if (DEBUG) Log.d(TAG, "using package default icon for " +
534                                     componentName.toShortString());
535                             entry.icon = packageEntry.icon;
536                             entry.title = packageEntry.title;
537                             entry.contentDescription = packageEntry.contentDescription;
538                         }
539                     }
540                     if (entry.icon == null) {
541                         if (DEBUG) Log.d(TAG, "using default icon for " +
542                                 componentName.toShortString());
543                         entry.icon = getDefaultIcon(user);
544                     }
545                 }
546             }
547 
548             if (TextUtils.isEmpty(entry.title)) {
549                 if (info == null && !providerFetchedOnce) {
550                     info = infoProvider.get();
551                     providerFetchedOnce = true;
552                 }
553                 if (info != null) {
554                     entry.title = info.getLabel();
555                     entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user);
556                 }
557             }
558         }
559         return entry;
560     }
561 
clear()562     public synchronized void clear() {
563         Preconditions.assertWorkerThread();
564         mIconDb.clear();
565     }
566 
567     /**
568      * Adds a default package entry in the cache. This entry is not persisted and will be removed
569      * when the cache is flushed.
570      */
cachePackageInstallInfo(String packageName, UserHandle user, Bitmap icon, CharSequence title)571     public synchronized void cachePackageInstallInfo(String packageName, UserHandle user,
572             Bitmap icon, CharSequence title) {
573         removeFromMemCacheLocked(packageName, user);
574 
575         ComponentKey cacheKey = getPackageKey(packageName, user);
576         CacheEntry entry = mCache.get(cacheKey);
577 
578         // For icon caching, do not go through DB. Just update the in-memory entry.
579         if (entry == null) {
580             entry = new CacheEntry();
581         }
582         if (!TextUtils.isEmpty(title)) {
583             entry.title = title;
584         }
585         if (icon != null) {
586             entry.icon = LauncherIcons.createIconBitmap(icon, mContext);
587         }
588         if (!TextUtils.isEmpty(title) && entry.icon != null) {
589             mCache.put(cacheKey, entry);
590         }
591     }
592 
getPackageKey(String packageName, UserHandle user)593     private static ComponentKey getPackageKey(String packageName, UserHandle user) {
594         ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME);
595         return new ComponentKey(cn, user);
596     }
597 
598     /**
599      * Gets an entry for the package, which can be used as a fallback entry for various components.
600      * This method is not thread safe, it must be called from a synchronized method.
601      */
getEntryForPackageLocked(String packageName, UserHandle user, boolean useLowResIcon)602     private CacheEntry getEntryForPackageLocked(String packageName, UserHandle user,
603             boolean useLowResIcon) {
604         Preconditions.assertWorkerThread();
605         ComponentKey cacheKey = getPackageKey(packageName, user);
606         CacheEntry entry = mCache.get(cacheKey);
607 
608         if (entry == null || (entry.isLowResIcon && !useLowResIcon)) {
609             entry = new CacheEntry();
610             boolean entryUpdated = true;
611 
612             // Check the DB first.
613             if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
614                 try {
615                     int flags = Process.myUserHandle().equals(user) ? 0 :
616                         PackageManager.GET_UNINSTALLED_PACKAGES;
617                     PackageInfo info = mPackageManager.getPackageInfo(packageName, flags);
618                     ApplicationInfo appInfo = info.applicationInfo;
619                     if (appInfo == null) {
620                         throw new NameNotFoundException("ApplicationInfo is null");
621                     }
622 
623                     // Load the full res icon for the application, but if useLowResIcon is set, then
624                     // only keep the low resolution icon instead of the larger full-sized icon
625                     Bitmap icon = LauncherIcons.createBadgedIconBitmap(
626                             appInfo.loadIcon(mPackageManager), user, mContext, appInfo.targetSdkVersion);
627                     if (mInstantAppResolver.isInstantApp(appInfo)) {
628                         icon = LauncherIcons.badgeWithDrawable(icon,
629                                 mContext.getDrawable(R.drawable.ic_instant_app_badge), mContext);
630                     }
631                     Bitmap lowResIcon =  generateLowResIcon(icon);
632                     entry.title = appInfo.loadLabel(mPackageManager);
633                     entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user);
634                     entry.icon = useLowResIcon ? lowResIcon : icon;
635                     entry.isLowResIcon = useLowResIcon;
636 
637                     // Add the icon in the DB here, since these do not get written during
638                     // package updates.
639                     ContentValues values =
640                             newContentValues(icon, lowResIcon, entry.title.toString(), packageName);
641                     addIconToDB(values, cacheKey.componentName, info,
642                             mUserManager.getSerialNumberForUser(user));
643 
644                 } catch (NameNotFoundException e) {
645                     if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
646                     entryUpdated = false;
647                 }
648             }
649 
650             // Only add a filled-out entry to the cache
651             if (entryUpdated) {
652                 mCache.put(cacheKey, entry);
653             }
654         }
655         return entry;
656     }
657 
getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes)658     private boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
659         Cursor c = null;
660         try {
661             c = mIconDb.query(
662                 new String[]{lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON,
663                         IconDB.COLUMN_LABEL},
664                 IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
665                 new String[]{cacheKey.componentName.flattenToString(),
666                         Long.toString(mUserManager.getSerialNumberForUser(cacheKey.user))});
667             if (c.moveToNext()) {
668                 entry.icon = loadIconNoResize(c, 0, lowRes ? mLowResOptions : null);
669                 entry.isLowResIcon = lowRes;
670                 entry.title = c.getString(1);
671                 if (entry.title == null) {
672                     entry.title = "";
673                     entry.contentDescription = "";
674                 } else {
675                     entry.contentDescription = mUserManager.getBadgedLabelForUser(
676                             entry.title, cacheKey.user);
677                 }
678                 return true;
679             }
680         } catch (SQLiteException e) {
681             Log.d(TAG, "Error reading icon cache", e);
682         } finally {
683             if (c != null) {
684                 c.close();
685             }
686         }
687         return false;
688     }
689 
690     public static class IconLoadRequest {
691         private final Runnable mRunnable;
692         private final Handler mHandler;
693 
IconLoadRequest(Runnable runnable, Handler handler)694         IconLoadRequest(Runnable runnable, Handler handler) {
695             mRunnable = runnable;
696             mHandler = handler;
697         }
698 
cancel()699         public void cancel() {
700             mHandler.removeCallbacks(mRunnable);
701         }
702     }
703 
704     /**
705      * A runnable that updates invalid icons and adds missing icons in the DB for the provided
706      * LauncherActivityInfo list. Items are updated/added one at a time, so that the
707      * worker thread doesn't get blocked.
708      */
709     @Thunk class SerializedIconUpdateTask implements Runnable {
710         private final long mUserSerial;
711         private final HashMap<String, PackageInfo> mPkgInfoMap;
712         private final Stack<LauncherActivityInfo> mAppsToAdd;
713         private final Stack<LauncherActivityInfo> mAppsToUpdate;
714         private final HashSet<String> mUpdatedPackages = new HashSet<>();
715 
SerializedIconUpdateTask(long userSerial, HashMap<String, PackageInfo> pkgInfoMap, Stack<LauncherActivityInfo> appsToAdd, Stack<LauncherActivityInfo> appsToUpdate)716         @Thunk SerializedIconUpdateTask(long userSerial, HashMap<String, PackageInfo> pkgInfoMap,
717                 Stack<LauncherActivityInfo> appsToAdd,
718                 Stack<LauncherActivityInfo> appsToUpdate) {
719             mUserSerial = userSerial;
720             mPkgInfoMap = pkgInfoMap;
721             mAppsToAdd = appsToAdd;
722             mAppsToUpdate = appsToUpdate;
723         }
724 
725         @Override
run()726         public void run() {
727             if (!mAppsToUpdate.isEmpty()) {
728                 LauncherActivityInfo app = mAppsToUpdate.pop();
729                 String pkg = app.getComponentName().getPackageName();
730                 PackageInfo info = mPkgInfoMap.get(pkg);
731                 addIconToDBAndMemCache(app, info, mUserSerial, true /*replace existing*/);
732                 mUpdatedPackages.add(pkg);
733 
734                 if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) {
735                     // No more app to update. Notify model.
736                     LauncherAppState.getInstance(mContext).getModel().onPackageIconsUpdated(
737                             mUpdatedPackages, mUserManager.getUserForSerialNumber(mUserSerial));
738                 }
739 
740                 // Let it run one more time.
741                 scheduleNext();
742             } else if (!mAppsToAdd.isEmpty()) {
743                 LauncherActivityInfo app = mAppsToAdd.pop();
744                 PackageInfo info = mPkgInfoMap.get(app.getComponentName().getPackageName());
745                 // We do not check the mPkgInfoMap when generating the mAppsToAdd. Although every
746                 // app should have package info, this is not guaranteed by the api
747                 if (info != null) {
748                     addIconToDBAndMemCache(app, info, mUserSerial, false /*replace existing*/);
749                 }
750 
751                 if (!mAppsToAdd.isEmpty()) {
752                     scheduleNext();
753                 }
754             }
755         }
756 
scheduleNext()757         public void scheduleNext() {
758             mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN, SystemClock.uptimeMillis() + 1);
759         }
760     }
761 
762     private static final class IconDB extends SQLiteCacheHelper {
763         private final static int DB_VERSION = 17;
764 
765         private final static int RELEASE_VERSION = DB_VERSION +
766                 (FeatureFlags.LAUNCHER3_DISABLE_ICON_NORMALIZATION ? 0 : 1);
767 
768         private final static String TABLE_NAME = "icons";
769         private final static String COLUMN_ROWID = "rowid";
770         private final static String COLUMN_COMPONENT = "componentName";
771         private final static String COLUMN_USER = "profileId";
772         private final static String COLUMN_LAST_UPDATED = "lastUpdated";
773         private final static String COLUMN_VERSION = "version";
774         private final static String COLUMN_ICON = "icon";
775         private final static String COLUMN_ICON_LOW_RES = "icon_low_res";
776         private final static String COLUMN_LABEL = "label";
777         private final static String COLUMN_SYSTEM_STATE = "system_state";
778 
IconDB(Context context, int iconPixelSize)779         public IconDB(Context context, int iconPixelSize) {
780             super(context, LauncherFiles.APP_ICONS_DB,
781                     (RELEASE_VERSION << 16) + iconPixelSize,
782                     TABLE_NAME);
783         }
784 
785         @Override
onCreateTable(SQLiteDatabase db)786         protected void onCreateTable(SQLiteDatabase db) {
787             db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
788                     COLUMN_COMPONENT + " TEXT NOT NULL, " +
789                     COLUMN_USER + " INTEGER NOT NULL, " +
790                     COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " +
791                     COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " +
792                     COLUMN_ICON + " BLOB, " +
793                     COLUMN_ICON_LOW_RES + " BLOB, " +
794                     COLUMN_LABEL + " TEXT, " +
795                     COLUMN_SYSTEM_STATE + " TEXT, " +
796                     "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " +
797                     ");");
798         }
799     }
800 
newContentValues(Bitmap icon, Bitmap lowResIcon, String label, String packageName)801     private ContentValues newContentValues(Bitmap icon, Bitmap lowResIcon, String label,
802             String packageName) {
803         ContentValues values = new ContentValues();
804         values.put(IconDB.COLUMN_ICON, Utilities.flattenBitmap(icon));
805         values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap(lowResIcon));
806 
807         values.put(IconDB.COLUMN_LABEL, label);
808         values.put(IconDB.COLUMN_SYSTEM_STATE, mIconProvider.getIconSystemState(packageName));
809 
810         return values;
811     }
812 
813     /**
814      * Generates a new low-res icon given a high-res icon.
815      */
generateLowResIcon(Bitmap icon)816     private Bitmap generateLowResIcon(Bitmap icon) {
817         return Bitmap.createScaledBitmap(icon,
818                 icon.getWidth() / LOW_RES_SCALE_FACTOR,
819                 icon.getHeight() / LOW_RES_SCALE_FACTOR, true);
820     }
821 
loadIconNoResize(Cursor c, int iconIndex, BitmapFactory.Options options)822     private static Bitmap loadIconNoResize(Cursor c, int iconIndex, BitmapFactory.Options options) {
823         byte[] data = c.getBlob(iconIndex);
824         try {
825             return BitmapFactory.decodeByteArray(data, 0, data.length, options);
826         } catch (Exception e) {
827             return null;
828         }
829     }
830 
831     private class ActivityInfoProvider extends Provider<LauncherActivityInfo> {
832 
833         private final Intent mIntent;
834         private final UserHandle mUser;
835 
ActivityInfoProvider(Intent intent, UserHandle user)836         public ActivityInfoProvider(Intent intent, UserHandle user) {
837             mIntent = intent;
838             mUser = user;
839         }
840 
841         @Override
get()842         public LauncherActivityInfo get() {
843             return mLauncherApps.resolveActivity(mIntent, mUser);
844         }
845     }
846 
847     /**
848      * Interface for receiving itemInfo with high-res icon.
849      */
850     public interface ItemInfoUpdateReceiver {
851 
reapplyItemInfo(ItemInfoWithIcon info)852         void reapplyItemInfo(ItemInfoWithIcon info);
853     }
854 }
855