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.app.ActivityManager;
     20 import android.content.ComponentName;
     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.PackageManager;
     26 import android.content.pm.PackageManager.NameNotFoundException;
     27 import android.content.res.Resources;
     28 import android.graphics.Bitmap;
     29 import android.graphics.BitmapFactory;
     30 import android.graphics.Canvas;
     31 import android.graphics.drawable.Drawable;
     32 import android.text.TextUtils;
     33 import android.util.Log;
     34 
     35 import com.android.launcher3.compat.LauncherActivityInfoCompat;
     36 import com.android.launcher3.compat.LauncherAppsCompat;
     37 import com.android.launcher3.compat.UserHandleCompat;
     38 import com.android.launcher3.compat.UserManagerCompat;
     39 
     40 import java.io.ByteArrayOutputStream;
     41 import java.io.File;
     42 import java.io.FileInputStream;
     43 import java.io.FileNotFoundException;
     44 import java.io.FileOutputStream;
     45 import java.io.IOException;
     46 import java.util.HashMap;
     47 import java.util.HashSet;
     48 import java.util.Iterator;
     49 import java.util.Map.Entry;
     50 
     51 /**
     52  * Cache of application icons.  Icons can be made from any thread.
     53  */
     54 public class IconCache {
     55 
     56     private static final String TAG = "Launcher.IconCache";
     57 
     58     private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
     59     private static final String RESOURCE_FILE_PREFIX = "icon_";
     60 
     61     // Empty class name is used for storing package default entry.
     62     private static final String EMPTY_CLASS_NAME = ".";
     63 
     64     private static final boolean DEBUG = false;
     65 
     66     private static class CacheEntry {
     67         public Bitmap icon;
     68         public CharSequence title;
     69         public CharSequence contentDescription;
     70     }
     71 
     72     private static class CacheKey {
     73         public ComponentName componentName;
     74         public UserHandleCompat user;
     75 
     76         CacheKey(ComponentName componentName, UserHandleCompat user) {
     77             this.componentName = componentName;
     78             this.user = user;
     79         }
     80 
     81         @Override
     82         public int hashCode() {
     83             return componentName.hashCode() + user.hashCode();
     84         }
     85 
     86         @Override
     87         public boolean equals(Object o) {
     88             CacheKey other = (CacheKey) o;
     89             return other.componentName.equals(componentName) && other.user.equals(user);
     90         }
     91     }
     92 
     93     private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons =
     94             new HashMap<UserHandleCompat, Bitmap>();
     95     private final Context mContext;
     96     private final PackageManager mPackageManager;
     97     private final UserManagerCompat mUserManager;
     98     private final LauncherAppsCompat mLauncherApps;
     99     private final HashMap<CacheKey, CacheEntry> mCache =
    100             new HashMap<CacheKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY);
    101     private int mIconDpi;
    102 
    103     public IconCache(Context context) {
    104         ActivityManager activityManager =
    105                 (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    106 
    107         mContext = context;
    108         mPackageManager = context.getPackageManager();
    109         mUserManager = UserManagerCompat.getInstance(mContext);
    110         mLauncherApps = LauncherAppsCompat.getInstance(mContext);
    111         mIconDpi = activityManager.getLauncherLargeIconDensity();
    112 
    113         // need to set mIconDpi before getting default icon
    114         UserHandleCompat myUser = UserHandleCompat.myUserHandle();
    115         mDefaultIcons.put(myUser, makeDefaultIcon(myUser));
    116     }
    117 
    118     public Drawable getFullResDefaultActivityIcon() {
    119         return getFullResIcon(Resources.getSystem(), android.R.mipmap.sym_def_app_icon);
    120     }
    121 
    122     private Drawable getFullResIcon(Resources resources, int iconId) {
    123         Drawable d;
    124         try {
    125             d = resources.getDrawableForDensity(iconId, mIconDpi);
    126         } catch (Resources.NotFoundException e) {
    127             d = null;
    128         }
    129 
    130         return (d != null) ? d : getFullResDefaultActivityIcon();
    131     }
    132 
    133     public Drawable getFullResIcon(String packageName, int iconId) {
    134         Resources resources;
    135         try {
    136             resources = mPackageManager.getResourcesForApplication(packageName);
    137         } catch (PackageManager.NameNotFoundException e) {
    138             resources = null;
    139         }
    140         if (resources != null) {
    141             if (iconId != 0) {
    142                 return getFullResIcon(resources, iconId);
    143             }
    144         }
    145         return getFullResDefaultActivityIcon();
    146     }
    147 
    148     public int getFullResIconDpi() {
    149         return mIconDpi;
    150     }
    151 
    152     public Drawable getFullResIcon(ActivityInfo info) {
    153         Resources resources;
    154         try {
    155             resources = mPackageManager.getResourcesForApplication(
    156                     info.applicationInfo);
    157         } catch (PackageManager.NameNotFoundException e) {
    158             resources = null;
    159         }
    160         if (resources != null) {
    161             int iconId = info.getIconResource();
    162             if (iconId != 0) {
    163                 return getFullResIcon(resources, iconId);
    164             }
    165         }
    166 
    167         return getFullResDefaultActivityIcon();
    168     }
    169 
    170     private Bitmap makeDefaultIcon(UserHandleCompat user) {
    171         Drawable unbadged = getFullResDefaultActivityIcon();
    172         Drawable d = mUserManager.getBadgedDrawableForUser(unbadged, user);
    173         Bitmap b = Bitmap.createBitmap(Math.max(d.getIntrinsicWidth(), 1),
    174                 Math.max(d.getIntrinsicHeight(), 1),
    175                 Bitmap.Config.ARGB_8888);
    176         Canvas c = new Canvas(b);
    177         d.setBounds(0, 0, b.getWidth(), b.getHeight());
    178         d.draw(c);
    179         c.setBitmap(null);
    180         return b;
    181     }
    182 
    183     /**
    184      * Remove any records for the supplied ComponentName.
    185      */
    186     public synchronized void remove(ComponentName componentName, UserHandleCompat user) {
    187         mCache.remove(new CacheKey(componentName, user));
    188     }
    189 
    190     /**
    191      * Remove any records for the supplied package name.
    192      */
    193     public synchronized void remove(String packageName, UserHandleCompat user) {
    194         HashSet<CacheKey> forDeletion = new HashSet<CacheKey>();
    195         for (CacheKey key: mCache.keySet()) {
    196             if (key.componentName.getPackageName().equals(packageName)
    197                     && key.user.equals(user)) {
    198                 forDeletion.add(key);
    199             }
    200         }
    201         for (CacheKey condemned: forDeletion) {
    202             mCache.remove(condemned);
    203         }
    204     }
    205 
    206     /**
    207      * Empty out the cache.
    208      */
    209     public synchronized void flush() {
    210         mCache.clear();
    211     }
    212 
    213     /**
    214      * Empty out the cache that aren't of the correct grid size
    215      */
    216     public synchronized void flushInvalidIcons(DeviceProfile grid) {
    217         Iterator<Entry<CacheKey, CacheEntry>> it = mCache.entrySet().iterator();
    218         while (it.hasNext()) {
    219             final CacheEntry e = it.next().getValue();
    220             if ((e.icon != null) && (e.icon.getWidth() < grid.iconSizePx
    221                     || e.icon.getHeight() < grid.iconSizePx)) {
    222                 it.remove();
    223             }
    224         }
    225     }
    226 
    227     /**
    228      * Fill in "application" with the icon and label for "info."
    229      */
    230     public synchronized void getTitleAndIcon(AppInfo application, LauncherActivityInfoCompat info,
    231             HashMap<Object, CharSequence> labelCache) {
    232         CacheEntry entry = cacheLocked(application.componentName, info, labelCache,
    233                 info.getUser(), false);
    234 
    235         application.title = entry.title;
    236         application.iconBitmap = entry.icon;
    237         application.contentDescription = entry.contentDescription;
    238     }
    239 
    240     public synchronized Bitmap getIcon(Intent intent, UserHandleCompat user) {
    241         ComponentName component = intent.getComponent();
    242         // null info means not installed, but if we have a component from the intent then
    243         // we should still look in the cache for restored app icons.
    244         if (component == null) {
    245             return getDefaultIcon(user);
    246         }
    247 
    248         LauncherActivityInfoCompat launcherActInfo = mLauncherApps.resolveActivity(intent, user);
    249         CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, true);
    250         return entry.icon;
    251     }
    252 
    253     /**
    254      * Fill in "shortcutInfo" with the icon and label for "info."
    255      */
    256     public synchronized void getTitleAndIcon(ShortcutInfo shortcutInfo, Intent intent,
    257             UserHandleCompat user, boolean usePkgIcon) {
    258         ComponentName component = intent.getComponent();
    259         // null info means not installed, but if we have a component from the intent then
    260         // we should still look in the cache for restored app icons.
    261         if (component == null) {
    262             shortcutInfo.setIcon(getDefaultIcon(user));
    263             shortcutInfo.title = "";
    264             shortcutInfo.usingFallbackIcon = true;
    265         } else {
    266             LauncherActivityInfoCompat launcherActInfo =
    267                     mLauncherApps.resolveActivity(intent, user);
    268             CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, usePkgIcon);
    269 
    270             shortcutInfo.setIcon(entry.icon);
    271             shortcutInfo.title = entry.title;
    272             shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user);
    273         }
    274     }
    275 
    276 
    277     public synchronized Bitmap getDefaultIcon(UserHandleCompat user) {
    278         if (!mDefaultIcons.containsKey(user)) {
    279             mDefaultIcons.put(user, makeDefaultIcon(user));
    280         }
    281         return mDefaultIcons.get(user);
    282     }
    283 
    284     public synchronized Bitmap getIcon(ComponentName component, LauncherActivityInfoCompat info,
    285             HashMap<Object, CharSequence> labelCache) {
    286         if (info == null || component == null) {
    287             return null;
    288         }
    289 
    290         CacheEntry entry = cacheLocked(component, info, labelCache, info.getUser(), false);
    291         return entry.icon;
    292     }
    293 
    294     public boolean isDefaultIcon(Bitmap icon, UserHandleCompat user) {
    295         return mDefaultIcons.get(user) == icon;
    296     }
    297 
    298     /**
    299      * Retrieves the entry from the cache. If the entry is not present, it creates a new entry.
    300      * This method is not thread safe, it must be called from a synchronized method.
    301      */
    302     private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info,
    303             HashMap<Object, CharSequence> labelCache, UserHandleCompat user, boolean usePackageIcon) {
    304         CacheKey cacheKey = new CacheKey(componentName, user);
    305         CacheEntry entry = mCache.get(cacheKey);
    306         if (entry == null) {
    307             entry = new CacheEntry();
    308 
    309             mCache.put(cacheKey, entry);
    310 
    311             if (info != null) {
    312                 ComponentName labelKey = info.getComponentName();
    313                 if (labelCache != null && labelCache.containsKey(labelKey)) {
    314                     entry.title = labelCache.get(labelKey).toString();
    315                 } else {
    316                     entry.title = info.getLabel().toString();
    317                     if (labelCache != null) {
    318                         labelCache.put(labelKey, entry.title);
    319                     }
    320                 }
    321 
    322                 entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user);
    323                 entry.icon = Utilities.createIconBitmap(
    324                         info.getBadgedIcon(mIconDpi), mContext);
    325             } else {
    326                 entry.title = "";
    327                 Bitmap preloaded = getPreloadedIcon(componentName, user);
    328                 if (preloaded != null) {
    329                     if (DEBUG) Log.d(TAG, "using preloaded icon for " +
    330                             componentName.toShortString());
    331                     entry.icon = preloaded;
    332                 } else {
    333                     if (usePackageIcon) {
    334                         CacheEntry packageEntry = getEntryForPackage(
    335                                 componentName.getPackageName(), user);
    336                         if (packageEntry != null) {
    337                             if (DEBUG) Log.d(TAG, "using package default icon for " +
    338                                     componentName.toShortString());
    339                             entry.icon = packageEntry.icon;
    340                             entry.title = packageEntry.title;
    341                         }
    342                     }
    343                     if (entry.icon == null) {
    344                         if (DEBUG) Log.d(TAG, "using default icon for " +
    345                                 componentName.toShortString());
    346                         entry.icon = getDefaultIcon(user);
    347                     }
    348                 }
    349             }
    350         }
    351         return entry;
    352     }
    353 
    354     /**
    355      * Adds a default package entry in the cache. This entry is not persisted and will be removed
    356      * when the cache is flushed.
    357      */
    358     public synchronized void cachePackageInstallInfo(String packageName, UserHandleCompat user,
    359             Bitmap icon, CharSequence title) {
    360         remove(packageName, user);
    361 
    362         CacheEntry entry = getEntryForPackage(packageName, user);
    363         if (!TextUtils.isEmpty(title)) {
    364             entry.title = title;
    365         }
    366         if (icon != null) {
    367             entry.icon = Utilities.createIconBitmap(icon, mContext);
    368         }
    369     }
    370 
    371     /**
    372      * Gets an entry for the package, which can be used as a fallback entry for various components.
    373      * This method is not thread safe, it must be called from a synchronized method.
    374      */
    375     private CacheEntry getEntryForPackage(String packageName, UserHandleCompat user) {
    376         ComponentName cn = new ComponentName(packageName, EMPTY_CLASS_NAME);;
    377         CacheKey cacheKey = new CacheKey(cn, user);
    378         CacheEntry entry = mCache.get(cacheKey);
    379         if (entry == null) {
    380             entry = new CacheEntry();
    381             entry.title = "";
    382             mCache.put(cacheKey, entry);
    383 
    384             try {
    385                 ApplicationInfo info = mPackageManager.getApplicationInfo(packageName, 0);
    386                 entry.title = info.loadLabel(mPackageManager);
    387                 entry.icon = Utilities.createIconBitmap(info.loadIcon(mPackageManager), mContext);
    388             } catch (NameNotFoundException e) {
    389                 if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
    390             }
    391 
    392             if (entry.icon == null) {
    393                 entry.icon = getPreloadedIcon(cn, user);
    394             }
    395         }
    396         return entry;
    397     }
    398 
    399     public synchronized HashMap<ComponentName,Bitmap> getAllIcons() {
    400         HashMap<ComponentName,Bitmap> set = new HashMap<ComponentName,Bitmap>();
    401         for (CacheKey ck : mCache.keySet()) {
    402             final CacheEntry e = mCache.get(ck);
    403             set.put(ck.componentName, e.icon);
    404         }
    405         return set;
    406     }
    407 
    408     /**
    409      * Pre-load an icon into the persistent cache.
    410      *
    411      * <P>Queries for a component that does not exist in the package manager
    412      * will be answered by the persistent cache.
    413      *
    414      * @param context application context
    415      * @param componentName the icon should be returned for this component
    416      * @param icon the icon to be persisted
    417      * @param dpi the native density of the icon
    418      */
    419     public static void preloadIcon(Context context, ComponentName componentName, Bitmap icon,
    420             int dpi) {
    421         // TODO rescale to the correct native DPI
    422         try {
    423             PackageManager packageManager = context.getPackageManager();
    424             packageManager.getActivityIcon(componentName);
    425             // component is present on the system already, do nothing
    426             return;
    427         } catch (PackageManager.NameNotFoundException e) {
    428             // pass
    429         }
    430 
    431         final String key = componentName.flattenToString();
    432         FileOutputStream resourceFile = null;
    433         try {
    434             resourceFile = context.openFileOutput(getResourceFilename(componentName),
    435                     Context.MODE_PRIVATE);
    436             ByteArrayOutputStream os = new ByteArrayOutputStream();
    437             if (icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 75, os)) {
    438                 byte[] buffer = os.toByteArray();
    439                 resourceFile.write(buffer, 0, buffer.length);
    440             } else {
    441                 Log.w(TAG, "failed to encode cache for " + key);
    442                 return;
    443             }
    444         } catch (FileNotFoundException e) {
    445             Log.w(TAG, "failed to pre-load cache for " + key, e);
    446         } catch (IOException e) {
    447             Log.w(TAG, "failed to pre-load cache for " + key, e);
    448         } finally {
    449             if (resourceFile != null) {
    450                 try {
    451                     resourceFile.close();
    452                 } catch (IOException e) {
    453                     Log.d(TAG, "failed to save restored icon for: " + key, e);
    454                 }
    455             }
    456         }
    457     }
    458 
    459     /**
    460      * Read a pre-loaded icon from the persistent icon cache.
    461      *
    462      * @param componentName the component that should own the icon
    463      * @returns a bitmap if one is cached, or null.
    464      */
    465     private Bitmap getPreloadedIcon(ComponentName componentName, UserHandleCompat user) {
    466         final String key = componentName.flattenToShortString();
    467 
    468         // We don't keep icons for other profiles in persistent cache.
    469         if (!user.equals(UserHandleCompat.myUserHandle())) {
    470             return null;
    471         }
    472 
    473         if (DEBUG) Log.v(TAG, "looking for pre-load icon for " + key);
    474         Bitmap icon = null;
    475         FileInputStream resourceFile = null;
    476         try {
    477             resourceFile = mContext.openFileInput(getResourceFilename(componentName));
    478             byte[] buffer = new byte[1024];
    479             ByteArrayOutputStream bytes = new ByteArrayOutputStream();
    480             int bytesRead = 0;
    481             while(bytesRead >= 0) {
    482                 bytes.write(buffer, 0, bytesRead);
    483                 bytesRead = resourceFile.read(buffer, 0, buffer.length);
    484             }
    485             if (DEBUG) Log.d(TAG, "read " + bytes.size());
    486             icon = BitmapFactory.decodeByteArray(bytes.toByteArray(), 0, bytes.size());
    487             if (icon == null) {
    488                 Log.w(TAG, "failed to decode pre-load icon for " + key);
    489             }
    490         } catch (FileNotFoundException e) {
    491             if (DEBUG) Log.d(TAG, "there is no restored icon for: " + key);
    492         } catch (IOException e) {
    493             Log.w(TAG, "failed to read pre-load icon for: " + key, e);
    494         } finally {
    495             if(resourceFile != null) {
    496                 try {
    497                     resourceFile.close();
    498                 } catch (IOException e) {
    499                     Log.d(TAG, "failed to manage pre-load icon file: " + key, e);
    500                 }
    501             }
    502         }
    503 
    504         return icon;
    505     }
    506 
    507     /**
    508      * Remove a pre-loaded icon from the persistent icon cache.
    509      *
    510      * @param componentName the component that should own the icon
    511      */
    512     public void deletePreloadedIcon(ComponentName componentName, UserHandleCompat user) {
    513         // We don't keep icons for other profiles in persistent cache.
    514         if (!user.equals(UserHandleCompat.myUserHandle()) || componentName == null) {
    515             return;
    516         }
    517         remove(componentName, user);
    518         boolean success = mContext.deleteFile(getResourceFilename(componentName));
    519         if (DEBUG && success) Log.d(TAG, "removed pre-loaded icon from persistent cache");
    520     }
    521 
    522     private static String getResourceFilename(ComponentName component) {
    523         String resourceName = component.flattenToShortString();
    524         String filename = resourceName.replace(File.separatorChar, '_');
    525         return RESOURCE_FILE_PREFIX + filename;
    526     }
    527 }
    528