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 
CacheKey(ComponentName componentName, UserHandleCompat user)76         CacheKey(ComponentName componentName, UserHandleCompat user) {
77             this.componentName = componentName;
78             this.user = user;
79         }
80 
81         @Override
hashCode()82         public int hashCode() {
83             return componentName.hashCode() + user.hashCode();
84         }
85 
86         @Override
equals(Object o)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 
IconCache(Context context)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 
getFullResDefaultActivityIcon()118     public Drawable getFullResDefaultActivityIcon() {
119         return getFullResIcon(Resources.getSystem(), android.R.mipmap.sym_def_app_icon);
120     }
121 
getFullResIcon(Resources resources, int iconId)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 
getFullResIcon(String packageName, int iconId)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 
getFullResIconDpi()148     public int getFullResIconDpi() {
149         return mIconDpi;
150     }
151 
getFullResIcon(ActivityInfo info)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 
makeDefaultIcon(UserHandleCompat user)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      */
remove(ComponentName componentName, UserHandleCompat user)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      */
remove(String packageName, UserHandleCompat user)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      */
flush()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      */
flushInvalidIcons(DeviceProfile grid)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      */
getTitleAndIcon(AppInfo application, LauncherActivityInfoCompat info, HashMap<Object, CharSequence> labelCache)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 
getIcon(Intent intent, UserHandleCompat user)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      */
getTitleAndIcon(ShortcutInfo shortcutInfo, Intent intent, UserHandleCompat user, boolean usePkgIcon)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 
getDefaultIcon(UserHandleCompat user)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 
getIcon(ComponentName component, LauncherActivityInfoCompat info, HashMap<Object, CharSequence> labelCache)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 
isDefaultIcon(Bitmap icon, UserHandleCompat user)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      */
cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info, HashMap<Object, CharSequence> labelCache, UserHandleCompat user, boolean usePackageIcon)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      */
cachePackageInstallInfo(String packageName, UserHandleCompat user, Bitmap icon, CharSequence title)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      */
getEntryForPackage(String packageName, UserHandleCompat user)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 
getAllIcons()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      */
preloadIcon(Context context, ComponentName componentName, Bitmap icon, int dpi)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      */
getPreloadedIcon(ComponentName componentName, UserHandleCompat user)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      */
deletePreloadedIcon(ComponentName componentName, UserHandleCompat user)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 
getResourceFilename(ComponentName component)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