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