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