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