OSDN Git Service

resolved conflicts for merge of 13ef17a3 to mnc-dr-dev
[android-x86/packages-apps-Launcher3.git] / src / com / android / launcher3 / IconCache.java
index 5a0875b..916418f 100644 (file)
 
 package com.android.launcher3;
 
-import android.app.ActivityManager;
 import android.content.ComponentName;
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.SystemClock;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -36,17 +45,17 @@ import com.android.launcher3.compat.LauncherActivityInfoCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.model.PackageItemInfo;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.Thunk;
 
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Map.Entry;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.Stack;
 
 /**
  * Cache of application icons.  Icons can be made from any thread.
@@ -56,66 +65,71 @@ public class IconCache {
     private static final String TAG = "Launcher.IconCache";
 
     private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
-    private static final String RESOURCE_FILE_PREFIX = "icon_";
 
     // Empty class name is used for storing package default entry.
     private static final String EMPTY_CLASS_NAME = ".";
 
     private static final boolean DEBUG = false;
 
-    private static class CacheEntry {
-        public Bitmap icon;
-        public CharSequence title;
-        public CharSequence contentDescription;
-    }
-
-    private static class CacheKey {
-        public ComponentName componentName;
-        public UserHandleCompat user;
+    private static final int LOW_RES_SCALE_FACTOR = 5;
 
-        CacheKey(ComponentName componentName, UserHandleCompat user) {
-            this.componentName = componentName;
-            this.user = user;
-        }
+    @Thunk static final Object ICON_UPDATE_TOKEN = new Object();
 
-        @Override
-        public int hashCode() {
-            return componentName.hashCode() + user.hashCode();
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            CacheKey other = (CacheKey) o;
-            return other.componentName.equals(componentName) && other.user.equals(user);
-        }
+    @Thunk static class CacheEntry {
+        public Bitmap icon;
+        public CharSequence title = "";
+        public CharSequence contentDescription = "";
+        public boolean isLowResIcon;
     }
 
-    private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons =
-            new HashMap<UserHandleCompat, Bitmap>();
+    private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons = new HashMap<>();
+    @Thunk final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
+
     private final Context mContext;
     private final PackageManager mPackageManager;
-    private final UserManagerCompat mUserManager;
+    @Thunk final UserManagerCompat mUserManager;
     private final LauncherAppsCompat mLauncherApps;
-    private final HashMap<CacheKey, CacheEntry> mCache =
-            new HashMap<CacheKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY);
-    private int mIconDpi;
-
-    public IconCache(Context context) {
-        ActivityManager activityManager =
-                (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
-
+    private final HashMap<ComponentKey, CacheEntry> mCache =
+            new HashMap<ComponentKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY);
+    private final int mIconDpi;
+    @Thunk final IconDB mIconDb;
+
+    @Thunk final Handler mWorkerHandler;
+
+    // The background color used for activity icons. Since these icons are displayed in all-apps
+    // and folders, this would be same as the light quantum panel background. This color
+    // is used to convert icons to RGB_565.
+    private final int mActivityBgColor;
+    // The background color used for package icons. These are displayed in widget tray, which
+    // has a dark quantum panel background.
+    private final int mPackageBgColor;
+    private final BitmapFactory.Options mLowResOptions;
+
+    private String mSystemState;
+    private Bitmap mLowResBitmap;
+    private Canvas mLowResCanvas;
+    private Paint mLowResPaint;
+
+    public IconCache(Context context, InvariantDeviceProfile inv) {
         mContext = context;
         mPackageManager = context.getPackageManager();
         mUserManager = UserManagerCompat.getInstance(mContext);
         mLauncherApps = LauncherAppsCompat.getInstance(mContext);
-        mIconDpi = activityManager.getLauncherLargeIconDensity();
-
-        // need to set mIconDpi before getting default icon
-        UserHandleCompat myUser = UserHandleCompat.myUserHandle();
-        mDefaultIcons.put(myUser, makeDefaultIcon(myUser));
+        mIconDpi = inv.fillResIconDpi;
+        mIconDb = new IconDB(context);
+
+        mWorkerHandler = new Handler(LauncherModel.getWorkerLooper());
+
+        mActivityBgColor = context.getResources().getColor(R.color.quantum_panel_bg_color);
+        mPackageBgColor = context.getResources().getColor(R.color.quantum_panel_bg_color_dark);
+        mLowResOptions = new BitmapFactory.Options();
+        // Always prefer RGB_565 config for low res. If the bitmap has transparency, it will
+        // automatically be loaded as ALPHA_8888.
+        mLowResOptions.inPreferredConfig = Bitmap.Config.RGB_565;
+        updateSystemStateString();
     }
 
-    public Drawable getFullResDefaultActivityIcon() {
+    private Drawable getFullResDefaultActivityIcon() {
         return getFullResIcon(Resources.getSystem(), android.R.mipmap.sym_def_app_icon);
     }
 
@@ -145,10 +159,6 @@ public class IconCache {
         return getFullResDefaultActivityIcon();
     }
 
-    public int getFullResIconDpi() {
-        return mIconDpi;
-    }
-
     public Drawable getFullResIcon(ActivityInfo info) {
         Resources resources;
         try {
@@ -184,59 +194,269 @@ public class IconCache {
      * Remove any records for the supplied ComponentName.
      */
     public synchronized void remove(ComponentName componentName, UserHandleCompat user) {
-        mCache.remove(new CacheKey(componentName, user));
+        mCache.remove(new ComponentKey(componentName, user));
     }
 
     /**
-     * Remove any records for the supplied package name.
+     * Remove any records for the supplied package name from memory.
      */
-    public synchronized void remove(String packageName, UserHandleCompat user) {
-        HashSet<CacheKey> forDeletion = new HashSet<CacheKey>();
-        for (CacheKey key: mCache.keySet()) {
+    private void removeFromMemCacheLocked(String packageName, UserHandleCompat user) {
+        HashSet<ComponentKey> forDeletion = new HashSet<ComponentKey>();
+        for (ComponentKey key: mCache.keySet()) {
             if (key.componentName.getPackageName().equals(packageName)
                     && key.user.equals(user)) {
                 forDeletion.add(key);
             }
         }
-        for (CacheKey condemned: forDeletion) {
+        for (ComponentKey condemned: forDeletion) {
             mCache.remove(condemned);
         }
     }
 
     /**
-     * Empty out the cache.
+     * Updates the entries related to the given package in memory and persistent DB.
      */
-    public synchronized void flush() {
-        mCache.clear();
+    public synchronized void updateIconsForPkg(String packageName, UserHandleCompat user) {
+        removeIconsForPkg(packageName, user);
+        try {
+            PackageInfo info = mPackageManager.getPackageInfo(packageName,
+                    PackageManager.GET_UNINSTALLED_PACKAGES);
+            long userSerial = mUserManager.getSerialNumberForUser(user);
+            for (LauncherActivityInfoCompat app : mLauncherApps.getActivityList(packageName, user)) {
+                addIconToDBAndMemCache(app, info, userSerial);
+            }
+        } catch (NameNotFoundException e) {
+            Log.d(TAG, "Package not found", e);
+            return;
+        }
     }
 
     /**
-     * Empty out the cache that aren't of the correct grid size
+     * Removes the entries related to the given package in memory and persistent DB.
      */
-    public synchronized void flushInvalidIcons(DeviceProfile grid) {
-        Iterator<Entry<CacheKey, CacheEntry>> it = mCache.entrySet().iterator();
-        while (it.hasNext()) {
-            final CacheEntry e = it.next().getValue();
-            if ((e.icon != null) && (e.icon.getWidth() < grid.iconSizePx
-                    || e.icon.getHeight() < grid.iconSizePx)) {
-                it.remove();
+    public synchronized void removeIconsForPkg(String packageName, UserHandleCompat user) {
+        removeFromMemCacheLocked(packageName, user);
+        long userSerial = mUserManager.getSerialNumberForUser(user);
+        mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME,
+                IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?",
+                new String[] {packageName + "/%", Long.toString(userSerial)});
+    }
+
+    public void updateDbIcons(Set<String> ignorePackagesForMainUser) {
+        // Remove all active icon update tasks.
+        mWorkerHandler.removeCallbacksAndMessages(ICON_UPDATE_TOKEN);
+
+        updateSystemStateString();
+        for (UserHandleCompat user : mUserManager.getUserProfiles()) {
+            // Query for the set of apps
+            final List<LauncherActivityInfoCompat> apps = mLauncherApps.getActivityList(null, user);
+            // Fail if we don't have any apps
+            // TODO: Fix this. Only fail for the current user.
+            if (apps == null || apps.isEmpty()) {
+                return;
             }
+
+            // Update icon cache. This happens in segments and {@link #onPackageIconsUpdated}
+            // is called by the icon cache when the job is complete.
+            updateDBIcons(user, apps, UserHandleCompat.myUserHandle().equals(user)
+                    ? ignorePackagesForMainUser : Collections.<String>emptySet());
         }
     }
 
     /**
-     * Fill in "application" with the icon and label for "info."
+     * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
+     * the DB and are updated.
+     * @return The set of packages for which icons have updated.
+     */
+    private void updateDBIcons(UserHandleCompat user, List<LauncherActivityInfoCompat> apps,
+            Set<String> ignorePackages) {
+        long userSerial = mUserManager.getSerialNumberForUser(user);
+        PackageManager pm = mContext.getPackageManager();
+        HashMap<String, PackageInfo> pkgInfoMap = new HashMap<String, PackageInfo>();
+        for (PackageInfo info : pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) {
+            pkgInfoMap.put(info.packageName, info);
+        }
+
+        HashMap<ComponentName, LauncherActivityInfoCompat> componentMap = new HashMap<>();
+        for (LauncherActivityInfoCompat app : apps) {
+            componentMap.put(app.getComponentName(), app);
+        }
+
+        Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME,
+                new String[] {IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT,
+                    IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION,
+                    IconDB.COLUMN_SYSTEM_STATE},
+                IconDB.COLUMN_USER + " = ? ",
+                new String[] {Long.toString(userSerial)},
+                null, null, null);
+
+        final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT);
+        final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED);
+        final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION);
+        final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID);
+        final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE);
+
+        HashSet<Integer> itemsToRemove = new HashSet<Integer>();
+        Stack<LauncherActivityInfoCompat> appsToUpdate = new Stack<>();
+
+        while (c.moveToNext()) {
+            String cn = c.getString(indexComponent);
+            ComponentName component = ComponentName.unflattenFromString(cn);
+            PackageInfo info = pkgInfoMap.get(component.getPackageName());
+            if (info == null) {
+                if (!ignorePackages.contains(component.getPackageName())) {
+                    remove(component, user);
+                    itemsToRemove.add(c.getInt(rowIndex));
+                }
+                continue;
+            }
+            if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) {
+                // Application is not present
+                continue;
+            }
+
+            long updateTime = c.getLong(indexLastUpdate);
+            int version = c.getInt(indexVersion);
+            LauncherActivityInfoCompat app = componentMap.remove(component);
+            if (version == info.versionCode && updateTime == info.lastUpdateTime &&
+                    TextUtils.equals(mSystemState, c.getString(systemStateIndex))) {
+                continue;
+            }
+            if (app == null) {
+                remove(component, user);
+                itemsToRemove.add(c.getInt(rowIndex));
+            } else {
+                appsToUpdate.add(app);
+            }
+        }
+        c.close();
+        if (!itemsToRemove.isEmpty()) {
+            mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME,
+                    Utilities.createDbSelectionQuery(IconDB.COLUMN_ROWID, itemsToRemove),
+                    null);
+        }
+
+        // Insert remaining apps.
+        if (!componentMap.isEmpty() || !appsToUpdate.isEmpty()) {
+            Stack<LauncherActivityInfoCompat> appsToAdd = new Stack<>();
+            appsToAdd.addAll(componentMap.values());
+            new SerializedIconUpdateTask(userSerial, pkgInfoMap,
+                    appsToAdd, appsToUpdate).scheduleNext();
+        }
+    }
+
+    @Thunk void addIconToDBAndMemCache(LauncherActivityInfoCompat app, PackageInfo info,
+            long userSerial) {
+        // Reuse the existing entry if it already exists in the DB. This ensures that we do not
+        // create bitmap if it was already created during loader.
+        ContentValues values = updateCacheAndGetContentValues(app, false);
+        addIconToDB(values, app.getComponentName(), info, userSerial);
+    }
+
+    /**
+     * Updates {@param values} to contain versoning information and adds it to the DB.
+     * @param values {@link ContentValues} containing icon & title
      */
-    public synchronized void getTitleAndIcon(AppInfo application, LauncherActivityInfoCompat info,
-            HashMap<Object, CharSequence> labelCache) {
-        CacheEntry entry = cacheLocked(application.componentName, info, labelCache,
-                info.getUser(), false);
+    private void addIconToDB(ContentValues values, ComponentName key,
+            PackageInfo info, long userSerial) {
+        values.put(IconDB.COLUMN_COMPONENT, key.flattenToString());
+        values.put(IconDB.COLUMN_USER, userSerial);
+        values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime);
+        values.put(IconDB.COLUMN_VERSION, info.versionCode);
+        mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values,
+                SQLiteDatabase.CONFLICT_REPLACE);
+    }
+
+    @Thunk ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app,
+            boolean replaceExisting) {
+        final ComponentKey key = new ComponentKey(app.getComponentName(), app.getUser());
+        CacheEntry entry = null;
+        if (!replaceExisting) {
+            entry = mCache.get(key);
+            // We can't reuse the entry if the high-res icon is not present.
+            if (entry == null || entry.isLowResIcon || entry.icon == null) {
+                entry = null;
+            }
+        }
+        if (entry == null) {
+            entry = new CacheEntry();
+            entry.icon = Utilities.createIconBitmap(app.getBadgedIcon(mIconDpi), mContext);
+        }
+        entry.title = app.getLabel();
+        entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, app.getUser());
+        mCache.put(new ComponentKey(app.getComponentName(), app.getUser()), entry);
+
+        return newContentValues(entry.icon, entry.title.toString(), mActivityBgColor);
+    }
+
+    /**
+     * Fetches high-res icon for the provided ItemInfo and updates the caller when done.
+     * @return a request ID that can be used to cancel the request.
+     */
+    public IconLoadRequest updateIconInBackground(final BubbleTextView caller, final ItemInfo info) {
+        Runnable request = new Runnable() {
+
+            @Override
+            public void run() {
+                if (info instanceof AppInfo) {
+                    getTitleAndIcon((AppInfo) info, null, false);
+                } else if (info instanceof ShortcutInfo) {
+                    ShortcutInfo st = (ShortcutInfo) info;
+                    getTitleAndIcon(st,
+                            st.promisedIntent != null ? st.promisedIntent : st.intent,
+                            st.user, false);
+                } else if (info instanceof PackageItemInfo) {
+                    PackageItemInfo pti = (PackageItemInfo) info;
+                    getTitleAndIconForApp(pti.packageName, pti.user, false, pti);
+                }
+                mMainThreadExecutor.execute(new Runnable() {
+
+                    @Override
+                    public void run() {
+                        caller.reapplyItemInfo(info);
+                    }
+                });
+            }
+        };
+        mWorkerHandler.post(request);
+        return new IconLoadRequest(request, mWorkerHandler);
+    }
 
-        application.title = entry.title;
-        application.iconBitmap = entry.icon;
+    private Bitmap getNonNullIcon(CacheEntry entry, UserHandleCompat user) {
+        return entry.icon == null ? getDefaultIcon(user) : entry.icon;
+    }
+
+    /**
+     * Fill in "application" with the icon and label for "info."
+     */
+    public synchronized void getTitleAndIcon(AppInfo application,
+            LauncherActivityInfoCompat info, boolean useLowResIcon) {
+        UserHandleCompat user = info == null ? application.user : info.getUser();
+        CacheEntry entry = cacheLocked(application.componentName, info, user,
+                false, useLowResIcon);
+        application.title = Utilities.trim(entry.title);
+        application.iconBitmap = getNonNullIcon(entry, user);
         application.contentDescription = entry.contentDescription;
+        application.usingLowResIcon = entry.isLowResIcon;
     }
 
+    /**
+     * Updates {@param application} only if a valid entry is found.
+     */
+    public synchronized void updateTitleAndIcon(AppInfo application) {
+        CacheEntry entry = cacheLocked(application.componentName, null, application.user,
+                false, application.usingLowResIcon);
+        if (entry.icon != null && !isDefaultIcon(entry.icon, application.user)) {
+            application.title = Utilities.trim(entry.title);
+            application.iconBitmap = entry.icon;
+            application.contentDescription = entry.contentDescription;
+            application.usingLowResIcon = entry.isLowResIcon;
+        }
+    }
+
+    /**
+     * Returns a high res icon for the given intent and user
+     */
     public synchronized Bitmap getIcon(Intent intent, UserHandleCompat user) {
         ComponentName component = intent.getComponent();
         // null info means not installed, but if we have a component from the intent then
@@ -246,15 +466,16 @@ public class IconCache {
         }
 
         LauncherActivityInfoCompat launcherActInfo = mLauncherApps.resolveActivity(intent, user);
-        CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, true);
+        CacheEntry entry = cacheLocked(component, launcherActInfo, user, true, false /* useLowRes */);
         return entry.icon;
     }
 
     /**
-     * Fill in "shortcutInfo" with the icon and label for "info."
+     * Fill in {@param shortcutInfo} with the icon and label for {@param intent}. If the
+     * corresponding activity is not found, it reverts to the package icon.
      */
     public synchronized void getTitleAndIcon(ShortcutInfo shortcutInfo, Intent intent,
-            UserHandleCompat user, boolean usePkgIcon) {
+            UserHandleCompat user, boolean useLowResIcon) {
         ComponentName component = intent.getComponent();
         // null info means not installed, but if we have a component from the intent then
         // we should still look in the cache for restored app icons.
@@ -262,17 +483,38 @@ public class IconCache {
             shortcutInfo.setIcon(getDefaultIcon(user));
             shortcutInfo.title = "";
             shortcutInfo.usingFallbackIcon = true;
+            shortcutInfo.usingLowResIcon = false;
         } else {
-            LauncherActivityInfoCompat launcherActInfo =
-                    mLauncherApps.resolveActivity(intent, user);
-            CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, usePkgIcon);
-
-            shortcutInfo.setIcon(entry.icon);
-            shortcutInfo.title = entry.title;
-            shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user);
+            LauncherActivityInfoCompat info = mLauncherApps.resolveActivity(intent, user);
+            getTitleAndIcon(shortcutInfo, component, info, user, true, useLowResIcon);
         }
     }
 
+    /**
+     * Fill in {@param shortcutInfo} with the icon and label for {@param info}
+     */
+    public synchronized void getTitleAndIcon(
+            ShortcutInfo shortcutInfo, ComponentName component, LauncherActivityInfoCompat info,
+            UserHandleCompat user, boolean usePkgIcon, boolean useLowResIcon) {
+        CacheEntry entry = cacheLocked(component, info, user, usePkgIcon, useLowResIcon);
+        shortcutInfo.setIcon(getNonNullIcon(entry, user));
+        shortcutInfo.title = Utilities.trim(entry.title);
+        shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user);
+        shortcutInfo.usingLowResIcon = entry.isLowResIcon;
+    }
+
+    /**
+     * Fill in {@param appInfo} with the icon and label for {@param packageName}
+     */
+    public synchronized void getTitleAndIconForApp(
+            String packageName, UserHandleCompat user, boolean useLowResIcon,
+            PackageItemInfo infoOut) {
+        CacheEntry entry = getEntryForPackageLocked(packageName, user, useLowResIcon);
+        infoOut.iconBitmap = getNonNullIcon(entry, user);
+        infoOut.title = Utilities.trim(entry.title);
+        infoOut.usingLowResIcon = entry.isLowResIcon;
+        infoOut.contentDescription = entry.contentDescription;
+    }
 
     public synchronized Bitmap getDefaultIcon(UserHandleCompat user) {
         if (!mDefaultIcons.containsKey(user)) {
@@ -281,16 +523,6 @@ public class IconCache {
         return mDefaultIcons.get(user);
     }
 
-    public synchronized Bitmap getIcon(ComponentName component, LauncherActivityInfoCompat info,
-            HashMap<Object, CharSequence> labelCache) {
-        if (info == null || component == null) {
-            return null;
-        }
-
-        CacheEntry entry = cacheLocked(component, info, labelCache, info.getUser(), false);
-        return entry.icon;
-    }
-
     public boolean isDefaultIcon(Bitmap icon, UserHandleCompat user) {
         return mDefaultIcons.get(user) == icon;
     }
@@ -300,44 +532,27 @@ public class IconCache {
      * This method is not thread safe, it must be called from a synchronized method.
      */
     private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info,
-            HashMap<Object, CharSequence> labelCache, UserHandleCompat user, boolean usePackageIcon) {
-        CacheKey cacheKey = new CacheKey(componentName, user);
+            UserHandleCompat user, boolean usePackageIcon, boolean useLowResIcon) {
+        ComponentKey cacheKey = new ComponentKey(componentName, user);
         CacheEntry entry = mCache.get(cacheKey);
-        if (entry == null) {
+        if (entry == null || (entry.isLowResIcon && !useLowResIcon)) {
             entry = new CacheEntry();
-
             mCache.put(cacheKey, entry);
 
-            if (info != null) {
-                ComponentName labelKey = info.getComponentName();
-                if (labelCache != null && labelCache.containsKey(labelKey)) {
-                    entry.title = labelCache.get(labelKey).toString();
-                } else {
-                    entry.title = info.getLabel().toString();
-                    if (labelCache != null) {
-                        labelCache.put(labelKey, entry.title);
-                    }
-                }
-
-                entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user);
-                entry.icon = Utilities.createIconBitmap(
-                        info.getBadgedIcon(mIconDpi), mContext);
-            } else {
-                entry.title = "";
-                Bitmap preloaded = getPreloadedIcon(componentName, user);
-                if (preloaded != null) {
-                    if (DEBUG) Log.d(TAG, "using preloaded icon for " +
-                            componentName.toShortString());
-                    entry.icon = preloaded;
+            // Check the DB first.
+            if (!getEntryFromDB(componentName, user, entry, useLowResIcon)) {
+                if (info != null) {
+                    entry.icon = Utilities.createIconBitmap(info.getBadgedIcon(mIconDpi), mContext);
                 } else {
                     if (usePackageIcon) {
-                        CacheEntry packageEntry = getEntryForPackage(
-                                componentName.getPackageName(), user);
+                        CacheEntry packageEntry = getEntryForPackageLocked(
+                                componentName.getPackageName(), user, false);
                         if (packageEntry != null) {
                             if (DEBUG) Log.d(TAG, "using package default icon for " +
                                     componentName.toShortString());
                             entry.icon = packageEntry.icon;
                             entry.title = packageEntry.title;
+                            entry.contentDescription = packageEntry.contentDescription;
                         }
                     }
                     if (entry.icon == null) {
@@ -347,6 +562,11 @@ public class IconCache {
                     }
                 }
             }
+
+            if (TextUtils.isEmpty(entry.title) && info != null) {
+                entry.title = info.getLabel();
+                entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user);
+            }
         }
         return entry;
     }
@@ -357,9 +577,9 @@ public class IconCache {
      */
     public synchronized void cachePackageInstallInfo(String packageName, UserHandleCompat user,
             Bitmap icon, CharSequence title) {
-        remove(packageName, user);
+        removeFromMemCacheLocked(packageName, user);
 
-        CacheEntry entry = getEntryForPackage(packageName, user);
+        CacheEntry entry = getEntryForPackageLocked(packageName, user, false);
         if (!TextUtils.isEmpty(title)) {
             entry.title = title;
         }
@@ -371,56 +591,68 @@ public class IconCache {
     /**
      * Gets an entry for the package, which can be used as a fallback entry for various components.
      * This method is not thread safe, it must be called from a synchronized method.
+     *
      */
-    private CacheEntry getEntryForPackage(String packageName, UserHandleCompat user) {
-        ComponentName cn = new ComponentName(packageName, EMPTY_CLASS_NAME);;
-        CacheKey cacheKey = new CacheKey(cn, user);
+    private CacheEntry getEntryForPackageLocked(String packageName, UserHandleCompat user,
+            boolean useLowResIcon) {
+        ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME);
+        ComponentKey cacheKey = new ComponentKey(cn, user);
         CacheEntry entry = mCache.get(cacheKey);
-        if (entry == null) {
+
+        if (entry == null || (entry.isLowResIcon && !useLowResIcon)) {
             entry = new CacheEntry();
-            entry.title = "";
-            mCache.put(cacheKey, entry);
+            boolean entryUpdated = true;
 
-            try {
-                ApplicationInfo info = mPackageManager.getApplicationInfo(packageName, 0);
-                entry.title = info.loadLabel(mPackageManager);
-                entry.icon = Utilities.createIconBitmap(info.loadIcon(mPackageManager), mContext);
-            } catch (NameNotFoundException e) {
-                if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
+            // Check the DB first.
+            if (!getEntryFromDB(cn, user, entry, useLowResIcon)) {
+                try {
+                    PackageInfo info = mPackageManager.getPackageInfo(packageName, 0);
+                    ApplicationInfo appInfo = info.applicationInfo;
+                    if (appInfo == null) {
+                        throw new NameNotFoundException("ApplicationInfo is null");
+                    }
+                    Drawable drawable = mUserManager.getBadgedDrawableForUser(
+                            appInfo.loadIcon(mPackageManager), user);
+                    entry.icon = Utilities.createIconBitmap(drawable, mContext);
+                    entry.title = appInfo.loadLabel(mPackageManager);
+                    entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user);
+                    entry.isLowResIcon = false;
+
+                    // Add the icon in the DB here, since these do not get written during
+                    // package updates.
+                    ContentValues values =
+                            newContentValues(entry.icon, entry.title.toString(), mPackageBgColor);
+                    addIconToDB(values, cn, info, mUserManager.getSerialNumberForUser(user));
+
+                } catch (NameNotFoundException e) {
+                    if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
+                    entryUpdated = false;
+                }
             }
 
-            if (entry.icon == null) {
-                entry.icon = getPreloadedIcon(cn, user);
+            // Only add a filled-out entry to the cache
+            if (entryUpdated) {
+                mCache.put(cacheKey, entry);
             }
         }
         return entry;
     }
 
-    public synchronized HashMap<ComponentName,Bitmap> getAllIcons() {
-        HashMap<ComponentName,Bitmap> set = new HashMap<ComponentName,Bitmap>();
-        for (CacheKey ck : mCache.keySet()) {
-            final CacheEntry e = mCache.get(ck);
-            set.put(ck.componentName, e.icon);
-        }
-        return set;
-    }
-
     /**
      * Pre-load an icon into the persistent cache.
      *
      * <P>Queries for a component that does not exist in the package manager
      * will be answered by the persistent cache.
      *
-     * @param context application context
      * @param componentName the icon should be returned for this component
      * @param icon the icon to be persisted
      * @param dpi the native density of the icon
      */
-    public static void preloadIcon(Context context, ComponentName componentName, Bitmap icon,
-            int dpi) {
+    public void preloadIcon(ComponentName componentName, Bitmap icon, int dpi, String label,
+            long userSerial) {
         // TODO rescale to the correct native DPI
         try {
-            PackageManager packageManager = context.getPackageManager();
+            PackageManager packageManager = mContext.getPackageManager();
             packageManager.getActivityIcon(componentName);
             // component is present on the system already, do nothing
             return;
@@ -428,100 +660,208 @@ public class IconCache {
             // pass
         }
 
-        final String key = componentName.flattenToString();
-        FileOutputStream resourceFile = null;
+        ContentValues values = newContentValues(icon, label, Color.TRANSPARENT);
+        values.put(IconDB.COLUMN_COMPONENT, componentName.flattenToString());
+        values.put(IconDB.COLUMN_USER, userSerial);
+        mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values,
+                SQLiteDatabase.CONFLICT_REPLACE);
+    }
+
+    private boolean getEntryFromDB(ComponentName component, UserHandleCompat user,
+            CacheEntry entry, boolean lowRes) {
+        Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME,
+                new String[] {lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON,
+                        IconDB.COLUMN_LABEL},
+                IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
+                new String[] {component.flattenToString(),
+                    Long.toString(mUserManager.getSerialNumberForUser(user))},
+                null, null, null);
         try {
-            resourceFile = context.openFileOutput(getResourceFilename(componentName),
-                    Context.MODE_PRIVATE);
-            ByteArrayOutputStream os = new ByteArrayOutputStream();
-            if (icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 75, os)) {
-                byte[] buffer = os.toByteArray();
-                resourceFile.write(buffer, 0, buffer.length);
-            } else {
-                Log.w(TAG, "failed to encode cache for " + key);
-                return;
-            }
-        } catch (FileNotFoundException e) {
-            Log.w(TAG, "failed to pre-load cache for " + key, e);
-        } catch (IOException e) {
-            Log.w(TAG, "failed to pre-load cache for " + key, e);
-        } finally {
-            if (resourceFile != null) {
-                try {
-                    resourceFile.close();
-                } catch (IOException e) {
-                    Log.d(TAG, "failed to save restored icon for: " + key, e);
+            if (c.moveToNext()) {
+                entry.icon = loadIconNoResize(c, 0, lowRes ? mLowResOptions : null);
+                entry.isLowResIcon = lowRes;
+                entry.title = c.getString(1);
+                if (entry.title == null) {
+                    entry.title = "";
+                    entry.contentDescription = "";
+                } else {
+                    entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user);
                 }
+                return true;
             }
+        } finally {
+            c.close();
+        }
+        return false;
+    }
+
+    public static class IconLoadRequest {
+        private final Runnable mRunnable;
+        private final Handler mHandler;
+
+        IconLoadRequest(Runnable runnable, Handler handler) {
+            mRunnable = runnable;
+            mHandler = handler;
+        }
+
+        public void cancel() {
+            mHandler.removeCallbacks(mRunnable);
         }
     }
 
     /**
-     * Read a pre-loaded icon from the persistent icon cache.
-     *
-     * @param componentName the component that should own the icon
-     * @returns a bitmap if one is cached, or null.
+     * A runnable that updates invalid icons and adds missing icons in the DB for the provided
+     * LauncherActivityInfoCompat list. Items are updated/added one at a time, so that the
+     * worker thread doesn't get blocked.
      */
-    private Bitmap getPreloadedIcon(ComponentName componentName, UserHandleCompat user) {
-        final String key = componentName.flattenToShortString();
-
-        // We don't keep icons for other profiles in persistent cache.
-        if (!user.equals(UserHandleCompat.myUserHandle())) {
-            return null;
+    @Thunk class SerializedIconUpdateTask implements Runnable {
+        private final long mUserSerial;
+        private final HashMap<String, PackageInfo> mPkgInfoMap;
+        private final Stack<LauncherActivityInfoCompat> mAppsToAdd;
+        private final Stack<LauncherActivityInfoCompat> mAppsToUpdate;
+        private final HashSet<String> mUpdatedPackages = new HashSet<String>();
+
+        @Thunk SerializedIconUpdateTask(long userSerial, HashMap<String, PackageInfo> pkgInfoMap,
+                Stack<LauncherActivityInfoCompat> appsToAdd,
+                Stack<LauncherActivityInfoCompat> appsToUpdate) {
+            mUserSerial = userSerial;
+            mPkgInfoMap = pkgInfoMap;
+            mAppsToAdd = appsToAdd;
+            mAppsToUpdate = appsToUpdate;
         }
 
-        if (DEBUG) Log.v(TAG, "looking for pre-load icon for " + key);
-        Bitmap icon = null;
-        FileInputStream resourceFile = null;
-        try {
-            resourceFile = mContext.openFileInput(getResourceFilename(componentName));
-            byte[] buffer = new byte[1024];
-            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-            int bytesRead = 0;
-            while(bytesRead >= 0) {
-                bytes.write(buffer, 0, bytesRead);
-                bytesRead = resourceFile.read(buffer, 0, buffer.length);
+        @Override
+        public void run() {
+            if (!mAppsToUpdate.isEmpty()) {
+                LauncherActivityInfoCompat app = mAppsToUpdate.pop();
+                String cn = app.getComponentName().flattenToString();
+                ContentValues values = updateCacheAndGetContentValues(app, true);
+                mIconDb.getWritableDatabase().update(IconDB.TABLE_NAME, values,
+                        IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
+                        new String[] {cn, Long.toString(mUserSerial)});
+                mUpdatedPackages.add(app.getComponentName().getPackageName());
+
+                if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) {
+                    // No more app to update. Notify model.
+                    LauncherAppState.getInstance().getModel().onPackageIconsUpdated(
+                            mUpdatedPackages, mUserManager.getUserForSerialNumber(mUserSerial));
+                }
+
+                // Let it run one more time.
+                scheduleNext();
+            } else if (!mAppsToAdd.isEmpty()) {
+                LauncherActivityInfoCompat app = mAppsToAdd.pop();
+                PackageInfo info = mPkgInfoMap.get(app.getComponentName().getPackageName());
+                if (info != null) {
+                    synchronized (IconCache.this) {
+                        addIconToDBAndMemCache(app, info, mUserSerial);
+                    }
+                }
+
+                if (!mAppsToAdd.isEmpty()) {
+                    scheduleNext();
+                }
             }
-            if (DEBUG) Log.d(TAG, "read " + bytes.size());
-            icon = BitmapFactory.decodeByteArray(bytes.toByteArray(), 0, bytes.size());
-            if (icon == null) {
-                Log.w(TAG, "failed to decode pre-load icon for " + key);
+        }
+
+        public void scheduleNext() {
+            mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN, SystemClock.uptimeMillis() + 1);
+        }
+    }
+
+    private void updateSystemStateString() {
+        mSystemState = Locale.getDefault().toString();
+    }
+
+    private static final class IconDB extends SQLiteOpenHelper {
+        private final static int DB_VERSION = 6;
+
+        private final static String TABLE_NAME = "icons";
+        private final static String COLUMN_ROWID = "rowid";
+        private final static String COLUMN_COMPONENT = "componentName";
+        private final static String COLUMN_USER = "profileId";
+        private final static String COLUMN_LAST_UPDATED = "lastUpdated";
+        private final static String COLUMN_VERSION = "version";
+        private final static String COLUMN_ICON = "icon";
+        private final static String COLUMN_ICON_LOW_RES = "icon_low_res";
+        private final static String COLUMN_LABEL = "label";
+        private final static String COLUMN_SYSTEM_STATE = "system_state";
+
+        public IconDB(Context context) {
+            super(context, LauncherFiles.APP_ICONS_DB, null, DB_VERSION);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
+                    COLUMN_COMPONENT + " TEXT NOT NULL, " +
+                    COLUMN_USER + " INTEGER NOT NULL, " +
+                    COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " +
+                    COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " +
+                    COLUMN_ICON + " BLOB, " +
+                    COLUMN_ICON_LOW_RES + " BLOB, " +
+                    COLUMN_LABEL + " TEXT, " +
+                    COLUMN_SYSTEM_STATE + " TEXT, " +
+                    "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " +
+                    ");");
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            if (oldVersion != newVersion) {
+                clearDB(db);
             }
-        } catch (FileNotFoundException e) {
-            if (DEBUG) Log.d(TAG, "there is no restored icon for: " + key);
-        } catch (IOException e) {
-            Log.w(TAG, "failed to read pre-load icon for: " + key, e);
-        } finally {
-            if(resourceFile != null) {
-                try {
-                    resourceFile.close();
-                } catch (IOException e) {
-                    Log.d(TAG, "failed to manage pre-load icon file: " + key, e);
-                }
+        }
+
+        @Override
+        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            if (oldVersion != newVersion) {
+                clearDB(db);
             }
         }
 
-        return icon;
+        private void clearDB(SQLiteDatabase db) {
+            db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
+            onCreate(db);
+        }
     }
 
-    /**
-     * Remove a pre-loaded icon from the persistent icon cache.
-     *
-     * @param componentName the component that should own the icon
-     */
-    public void deletePreloadedIcon(ComponentName componentName, UserHandleCompat user) {
-        // We don't keep icons for other profiles in persistent cache.
-        if (!user.equals(UserHandleCompat.myUserHandle()) || componentName == null) {
-            return;
+    private ContentValues newContentValues(Bitmap icon, String label, int lowResBackgroundColor) {
+        ContentValues values = new ContentValues();
+        values.put(IconDB.COLUMN_ICON, Utilities.flattenBitmap(icon));
+
+        values.put(IconDB.COLUMN_LABEL, label);
+        values.put(IconDB.COLUMN_SYSTEM_STATE, mSystemState);
+
+        if (lowResBackgroundColor == Color.TRANSPARENT) {
+          values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap(
+          Bitmap.createScaledBitmap(icon,
+                  icon.getWidth() / LOW_RES_SCALE_FACTOR,
+                  icon.getHeight() / LOW_RES_SCALE_FACTOR, true)));
+        } else {
+            synchronized (this) {
+                if (mLowResBitmap == null) {
+                    mLowResBitmap = Bitmap.createBitmap(icon.getWidth() / LOW_RES_SCALE_FACTOR,
+                            icon.getHeight() / LOW_RES_SCALE_FACTOR, Bitmap.Config.RGB_565);
+                    mLowResCanvas = new Canvas(mLowResBitmap);
+                    mLowResPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
+                }
+                mLowResCanvas.drawColor(lowResBackgroundColor);
+                mLowResCanvas.drawBitmap(icon, new Rect(0, 0, icon.getWidth(), icon.getHeight()),
+                        new Rect(0, 0, mLowResBitmap.getWidth(), mLowResBitmap.getHeight()),
+                        mLowResPaint);
+                values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap(mLowResBitmap));
+            }
         }
-        remove(componentName, user);
-        boolean success = mContext.deleteFile(getResourceFilename(componentName));
-        if (DEBUG && success) Log.d(TAG, "removed pre-loaded icon from persistent cache");
+        return values;
     }
 
-    private static String getResourceFilename(ComponentName component) {
-        String resourceName = component.flattenToShortString();
-        String filename = resourceName.replace(File.separatorChar, '_');
-        return RESOURCE_FILE_PREFIX + filename;
+    private static Bitmap loadIconNoResize(Cursor c, int iconIndex, BitmapFactory.Options options) {
+        byte[] data = c.getBlob(iconIndex);
+        try {
+            return BitmapFactory.decodeByteArray(data, 0, data.length, options);
+        } catch (Exception e) {
+            return null;
+        }
     }
 }