Home | History | Annotate | Download | only in launcher3
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.launcher3;
     18 
     19 import android.content.ComponentName;
     20 import android.content.ContentValues;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.pm.ActivityInfo;
     24 import android.content.pm.ApplicationInfo;
     25 import android.content.pm.PackageInfo;
     26 import android.content.pm.PackageManager;
     27 import android.content.pm.PackageManager.NameNotFoundException;
     28 import android.content.res.Resources;
     29 import android.database.Cursor;
     30 import android.database.sqlite.SQLiteDatabase;
     31 import android.database.sqlite.SQLiteOpenHelper;
     32 import android.graphics.Bitmap;
     33 import android.graphics.BitmapFactory;
     34 import android.graphics.Canvas;
     35 import android.graphics.Color;
     36 import android.graphics.Paint;
     37 import android.graphics.Rect;
     38 import android.graphics.drawable.Drawable;
     39 import android.os.Handler;
     40 import android.os.SystemClock;
     41 import android.text.TextUtils;
     42 import android.util.Log;
     43 
     44 import com.android.launcher3.compat.LauncherActivityInfoCompat;
     45 import com.android.launcher3.compat.LauncherAppsCompat;
     46 import com.android.launcher3.compat.UserHandleCompat;
     47 import com.android.launcher3.compat.UserManagerCompat;
     48 import com.android.launcher3.model.PackageItemInfo;
     49 import com.android.launcher3.util.ComponentKey;
     50 import com.android.launcher3.util.Thunk;
     51 
     52 import java.util.Collections;
     53 import java.util.HashMap;
     54 import java.util.HashSet;
     55 import java.util.List;
     56 import java.util.Locale;
     57 import java.util.Set;
     58 import java.util.Stack;
     59 
     60 /**
     61  * Cache of application icons.  Icons can be made from any thread.
     62  */
     63 public 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(componentName, user, 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         CacheEntry entry = getEntryForPackageLocked(packageName, user, false);
    583         if (!TextUtils.isEmpty(title)) {
    584             entry.title = title;
    585         }
    586         if (icon != null) {
    587             entry.icon = Utilities.createIconBitmap(icon, mContext);
    588         }
    589     }
    590 
    591     /**
    592      * Gets an entry for the package, which can be used as a fallback entry for various components.
    593      * This method is not thread safe, it must be called from a synchronized method.
    594      *
    595      */
    596     private CacheEntry getEntryForPackageLocked(String packageName, UserHandleCompat user,
    597             boolean useLowResIcon) {
    598         ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME);
    599         ComponentKey cacheKey = new ComponentKey(cn, user);
    600         CacheEntry entry = mCache.get(cacheKey);
    601 
    602         if (entry == null || (entry.isLowResIcon && !useLowResIcon)) {
    603             entry = new CacheEntry();
    604             boolean entryUpdated = true;
    605 
    606             // Check the DB first.
    607             if (!getEntryFromDB(cn, user, entry, useLowResIcon)) {
    608                 try {
    609                     PackageInfo info = mPackageManager.getPackageInfo(packageName, 0);
    610                     ApplicationInfo appInfo = info.applicationInfo;
    611                     if (appInfo == null) {
    612                         throw new NameNotFoundException("ApplicationInfo is null");
    613                     }
    614                     Drawable drawable = mUserManager.getBadgedDrawableForUser(
    615                             appInfo.loadIcon(mPackageManager), user);
    616                     entry.icon = Utilities.createIconBitmap(drawable, mContext);
    617                     entry.title = appInfo.loadLabel(mPackageManager);
    618                     entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user);
    619                     entry.isLowResIcon = false;
    620 
    621                     // Add the icon in the DB here, since these do not get written during
    622                     // package updates.
    623                     ContentValues values =
    624                             newContentValues(entry.icon, entry.title.toString(), mPackageBgColor);
    625                     addIconToDB(values, cn, info, mUserManager.getSerialNumberForUser(user));
    626 
    627                 } catch (NameNotFoundException e) {
    628                     if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
    629                     entryUpdated = false;
    630                 }
    631             }
    632 
    633             // Only add a filled-out entry to the cache
    634             if (entryUpdated) {
    635                 mCache.put(cacheKey, entry);
    636             }
    637         }
    638         return entry;
    639     }
    640 
    641     /**
    642      * Pre-load an icon into the persistent cache.
    643      *
    644      * <P>Queries for a component that does not exist in the package manager
    645      * will be answered by the persistent cache.
    646      *
    647      * @param componentName the icon should be returned for this component
    648      * @param icon the icon to be persisted
    649      * @param dpi the native density of the icon
    650      */
    651     public void preloadIcon(ComponentName componentName, Bitmap icon, int dpi, String label,
    652             long userSerial) {
    653         // TODO rescale to the correct native DPI
    654         try {
    655             PackageManager packageManager = mContext.getPackageManager();
    656             packageManager.getActivityIcon(componentName);
    657             // component is present on the system already, do nothing
    658             return;
    659         } catch (PackageManager.NameNotFoundException e) {
    660             // pass
    661         }
    662 
    663         ContentValues values = newContentValues(icon, label, Color.TRANSPARENT);
    664         values.put(IconDB.COLUMN_COMPONENT, componentName.flattenToString());
    665         values.put(IconDB.COLUMN_USER, userSerial);
    666         mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values,
    667                 SQLiteDatabase.CONFLICT_REPLACE);
    668     }
    669 
    670     private boolean getEntryFromDB(ComponentName component, UserHandleCompat user,
    671             CacheEntry entry, boolean lowRes) {
    672         Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME,
    673                 new String[] {lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON,
    674                         IconDB.COLUMN_LABEL},
    675                 IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
    676                 new String[] {component.flattenToString(),
    677                     Long.toString(mUserManager.getSerialNumberForUser(user))},
    678                 null, null, null);
    679         try {
    680             if (c.moveToNext()) {
    681                 entry.icon = loadIconNoResize(c, 0, lowRes ? mLowResOptions : null);
    682                 entry.isLowResIcon = lowRes;
    683                 entry.title = c.getString(1);
    684                 if (entry.title == null) {
    685                     entry.title = "";
    686                     entry.contentDescription = "";
    687                 } else {
    688                     entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user);
    689                 }
    690                 return true;
    691             }
    692         } finally {
    693             c.close();
    694         }
    695         return false;
    696     }
    697 
    698     public static class IconLoadRequest {
    699         private final Runnable mRunnable;
    700         private final Handler mHandler;
    701 
    702         IconLoadRequest(Runnable runnable, Handler handler) {
    703             mRunnable = runnable;
    704             mHandler = handler;
    705         }
    706 
    707         public void cancel() {
    708             mHandler.removeCallbacks(mRunnable);
    709         }
    710     }
    711 
    712     /**
    713      * A runnable that updates invalid icons and adds missing icons in the DB for the provided
    714      * LauncherActivityInfoCompat list. Items are updated/added one at a time, so that the
    715      * worker thread doesn't get blocked.
    716      */
    717     @Thunk class SerializedIconUpdateTask implements Runnable {
    718         private final long mUserSerial;
    719         private final HashMap<String, PackageInfo> mPkgInfoMap;
    720         private final Stack<LauncherActivityInfoCompat> mAppsToAdd;
    721         private final Stack<LauncherActivityInfoCompat> mAppsToUpdate;
    722         private final HashSet<String> mUpdatedPackages = new HashSet<String>();
    723 
    724         @Thunk SerializedIconUpdateTask(long userSerial, HashMap<String, PackageInfo> pkgInfoMap,
    725                 Stack<LauncherActivityInfoCompat> appsToAdd,
    726                 Stack<LauncherActivityInfoCompat> appsToUpdate) {
    727             mUserSerial = userSerial;
    728             mPkgInfoMap = pkgInfoMap;
    729             mAppsToAdd = appsToAdd;
    730             mAppsToUpdate = appsToUpdate;
    731         }
    732 
    733         @Override
    734         public void run() {
    735             if (!mAppsToUpdate.isEmpty()) {
    736                 LauncherActivityInfoCompat app = mAppsToUpdate.pop();
    737                 String cn = app.getComponentName().flattenToString();
    738                 ContentValues values = updateCacheAndGetContentValues(app, true);
    739                 mIconDb.getWritableDatabase().update(IconDB.TABLE_NAME, values,
    740                         IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
    741                         new String[] {cn, Long.toString(mUserSerial)});
    742                 mUpdatedPackages.add(app.getComponentName().getPackageName());
    743 
    744                 if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) {
    745                     // No more app to update. Notify model.
    746                     LauncherAppState.getInstance().getModel().onPackageIconsUpdated(
    747                             mUpdatedPackages, mUserManager.getUserForSerialNumber(mUserSerial));
    748                 }
    749 
    750                 // Let it run one more time.
    751                 scheduleNext();
    752             } else if (!mAppsToAdd.isEmpty()) {
    753                 LauncherActivityInfoCompat app = mAppsToAdd.pop();
    754                 PackageInfo info = mPkgInfoMap.get(app.getComponentName().getPackageName());
    755                 if (info != null) {
    756                     synchronized (IconCache.this) {
    757                         addIconToDBAndMemCache(app, info, mUserSerial);
    758                     }
    759                 }
    760 
    761                 if (!mAppsToAdd.isEmpty()) {
    762                     scheduleNext();
    763                 }
    764             }
    765         }
    766 
    767         public void scheduleNext() {
    768             mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN, SystemClock.uptimeMillis() + 1);
    769         }
    770     }
    771 
    772     private void updateSystemStateString() {
    773         mSystemState = Locale.getDefault().toString();
    774     }
    775 
    776     private static final class IconDB extends SQLiteOpenHelper {
    777         private final static int DB_VERSION = 6;
    778 
    779         private final static String TABLE_NAME = "icons";
    780         private final static String COLUMN_ROWID = "rowid";
    781         private final static String COLUMN_COMPONENT = "componentName";
    782         private final static String COLUMN_USER = "profileId";
    783         private final static String COLUMN_LAST_UPDATED = "lastUpdated";
    784         private final static String COLUMN_VERSION = "version";
    785         private final static String COLUMN_ICON = "icon";
    786         private final static String COLUMN_ICON_LOW_RES = "icon_low_res";
    787         private final static String COLUMN_LABEL = "label";
    788         private final static String COLUMN_SYSTEM_STATE = "system_state";
    789 
    790         public IconDB(Context context) {
    791             super(context, LauncherFiles.APP_ICONS_DB, null, DB_VERSION);
    792         }
    793 
    794         @Override
    795         public void onCreate(SQLiteDatabase db) {
    796             db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
    797                     COLUMN_COMPONENT + " TEXT NOT NULL, " +
    798                     COLUMN_USER + " INTEGER NOT NULL, " +
    799                     COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " +
    800                     COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " +
    801                     COLUMN_ICON + " BLOB, " +
    802                     COLUMN_ICON_LOW_RES + " BLOB, " +
    803                     COLUMN_LABEL + " TEXT, " +
    804                     COLUMN_SYSTEM_STATE + " TEXT, " +
    805                     "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " +
    806                     ");");
    807         }
    808 
    809         @Override
    810         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    811             if (oldVersion != newVersion) {
    812                 clearDB(db);
    813             }
    814         }
    815 
    816         @Override
    817         public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    818             if (oldVersion != newVersion) {
    819                 clearDB(db);
    820             }
    821         }
    822 
    823         private void clearDB(SQLiteDatabase db) {
    824             db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
    825             onCreate(db);
    826         }
    827     }
    828 
    829     private ContentValues newContentValues(Bitmap icon, String label, int lowResBackgroundColor) {
    830         ContentValues values = new ContentValues();
    831         values.put(IconDB.COLUMN_ICON, Utilities.flattenBitmap(icon));
    832 
    833         values.put(IconDB.COLUMN_LABEL, label);
    834         values.put(IconDB.COLUMN_SYSTEM_STATE, mSystemState);
    835 
    836         if (lowResBackgroundColor == Color.TRANSPARENT) {
    837           values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap(
    838           Bitmap.createScaledBitmap(icon,
    839                   icon.getWidth() / LOW_RES_SCALE_FACTOR,
    840                   icon.getHeight() / LOW_RES_SCALE_FACTOR, true)));
    841         } else {
    842             synchronized (this) {
    843                 if (mLowResBitmap == null) {
    844                     mLowResBitmap = Bitmap.createBitmap(icon.getWidth() / LOW_RES_SCALE_FACTOR,
    845                             icon.getHeight() / LOW_RES_SCALE_FACTOR, Bitmap.Config.RGB_565);
    846                     mLowResCanvas = new Canvas(mLowResBitmap);
    847                     mLowResPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
    848                 }
    849                 mLowResCanvas.drawColor(lowResBackgroundColor);
    850                 mLowResCanvas.drawBitmap(icon, new Rect(0, 0, icon.getWidth(), icon.getHeight()),
    851                         new Rect(0, 0, mLowResBitmap.getWidth(), mLowResBitmap.getHeight()),
    852                         mLowResPaint);
    853                 values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap(mLowResBitmap));
    854             }
    855         }
    856         return values;
    857     }
    858 
    859     private static Bitmap loadIconNoResize(Cursor c, int iconIndex, BitmapFactory.Options options) {
    860         byte[] data = c.getBlob(iconIndex);
    861         try {
    862             return BitmapFactory.decodeByteArray(data, 0, data.length, options);
    863         } catch (Exception e) {
    864             return null;
    865         }
    866     }
    867 }
    868