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