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