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