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
17package com.android.launcher3;
18
19import android.content.ComponentName;
20import android.content.ContentValues;
21import android.content.Context;
22import android.content.Intent;
23import android.content.pm.ActivityInfo;
24import android.content.pm.ApplicationInfo;
25import android.content.pm.PackageInfo;
26import android.content.pm.PackageManager;
27import android.content.pm.PackageManager.NameNotFoundException;
28import android.content.res.Resources;
29import android.content.res.TypedArray;
30import android.database.Cursor;
31import android.database.sqlite.SQLiteDatabase;
32import android.database.sqlite.SQLiteException;
33import android.graphics.Bitmap;
34import android.graphics.BitmapFactory;
35import android.graphics.Canvas;
36import android.graphics.Color;
37import android.graphics.Paint;
38import android.graphics.Rect;
39import android.graphics.drawable.Drawable;
40import android.os.Handler;
41import android.os.SystemClock;
42import android.text.TextUtils;
43import android.util.Log;
44
45import com.android.launcher3.compat.LauncherActivityInfoCompat;
46import com.android.launcher3.compat.LauncherAppsCompat;
47import com.android.launcher3.compat.UserHandleCompat;
48import com.android.launcher3.compat.UserManagerCompat;
49import com.android.launcher3.config.FeatureFlags;
50import com.android.launcher3.model.PackageItemInfo;
51import com.android.launcher3.util.ComponentKey;
52import com.android.launcher3.util.SQLiteCacheHelper;
53import com.android.launcher3.util.Thunk;
54
55import java.util.Collections;
56import java.util.HashMap;
57import java.util.HashSet;
58import java.util.List;
59import java.util.Set;
60import java.util.Stack;
61
62/**
63 * Cache of application icons.  Icons can be made from any thread.
64 */
65public 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
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
140    private Drawable getFullResDefaultActivityIcon() {
141        return getFullResIcon(Resources.getSystem(), android.R.mipmap.sym_def_app_icon);
142    }
143
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
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
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
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     */
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     */
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     */
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     */
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
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     */
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
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     */
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
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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     */
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     */
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
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     */
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     */
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
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
740        IconLoadRequest(Runnable runnable, Handler handler) {
741            mRunnable = runnable;
742            mHandler = handler;
743        }
744
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
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
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
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
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
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
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     */
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
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