IconCache.java revision ed13187a745866483139e2878037e1f8427ce567
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 String title;
68    }
69
70    private static class CacheKey {
71        public ComponentName componentName;
72        public UserHandleCompat user;
73
74        CacheKey(ComponentName componentName, UserHandleCompat user) {
75            this.componentName = componentName;
76            this.user = user;
77        }
78
79        @Override
80        public int hashCode() {
81            return componentName.hashCode() + user.hashCode();
82        }
83
84        @Override
85        public boolean equals(Object o) {
86            CacheKey other = (CacheKey) o;
87            return other.componentName.equals(componentName) && other.user.equals(user);
88        }
89    }
90
91    private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons =
92            new HashMap<UserHandleCompat, Bitmap>();
93    private final Context mContext;
94    private final PackageManager mPackageManager;
95    private final UserManagerCompat mUserManager;
96    private final LauncherAppsCompat mLauncherApps;
97    private final HashMap<CacheKey, CacheEntry> mCache =
98            new HashMap<CacheKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY);
99    private int mIconDpi;
100
101    public IconCache(Context context) {
102        ActivityManager activityManager =
103                (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
104
105        mContext = context;
106        mPackageManager = context.getPackageManager();
107        mUserManager = UserManagerCompat.getInstance(mContext);
108        mLauncherApps = LauncherAppsCompat.getInstance(mContext);
109        mIconDpi = activityManager.getLauncherLargeIconDensity();
110
111        // need to set mIconDpi before getting default icon
112        UserHandleCompat myUser = UserHandleCompat.myUserHandle();
113        mDefaultIcons.put(myUser, makeDefaultIcon(myUser));
114    }
115
116    public Drawable getFullResDefaultActivityIcon() {
117        return getFullResIcon(Resources.getSystem(),
118                android.R.mipmap.sym_def_app_icon);
119    }
120
121    public Drawable getFullResIcon(Resources resources, int iconId) {
122        Drawable d;
123        try {
124            d = resources.getDrawableForDensity(iconId, mIconDpi);
125        } catch (Resources.NotFoundException e) {
126            d = null;
127        }
128
129        return (d != null) ? d : getFullResDefaultActivityIcon();
130    }
131
132    public Drawable getFullResIcon(String packageName, int iconId) {
133        Resources resources;
134        try {
135            resources = mPackageManager.getResourcesForApplication(packageName);
136        } catch (PackageManager.NameNotFoundException e) {
137            resources = null;
138        }
139        if (resources != null) {
140            if (iconId != 0) {
141                return getFullResIcon(resources, iconId);
142            }
143        }
144        return getFullResDefaultActivityIcon();
145    }
146
147    public Drawable getFullResIcon(ResolveInfo info) {
148        return getFullResIcon(info.activityInfo);
149    }
150
151    public Drawable getFullResIcon(ActivityInfo info) {
152
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 void remove(ComponentName componentName, UserHandleCompat user) {
187        synchronized (mCache) {
188            mCache.remove(new CacheKey(componentName, user));
189        }
190    }
191
192    /**
193     * Remove any records for the supplied package name.
194     */
195    public void remove(String packageName, UserHandleCompat user) {
196        HashSet<CacheKey> forDeletion = new HashSet<CacheKey>();
197        for (CacheKey key: mCache.keySet()) {
198            if (key.componentName.getPackageName().equals(packageName)
199                    && key.user.equals(user)) {
200                forDeletion.add(key);
201            }
202        }
203        for (CacheKey condemned: forDeletion) {
204            mCache.remove(condemned);
205        }
206    }
207
208    /**
209     * Empty out the cache.
210     */
211    public void flush() {
212        synchronized (mCache) {
213            mCache.clear();
214        }
215    }
216
217    /**
218     * Empty out the cache that aren't of the correct grid size
219     */
220    public void flushInvalidIcons(DeviceProfile grid) {
221        synchronized (mCache) {
222            Iterator<Entry<CacheKey, CacheEntry>> it = mCache.entrySet().iterator();
223            while (it.hasNext()) {
224                final CacheEntry e = it.next().getValue();
225                if (e.icon.getWidth() < grid.iconSizePx || e.icon.getHeight() < grid.iconSizePx) {
226                    it.remove();
227                }
228            }
229        }
230    }
231
232    /**
233     * Fill in "application" with the icon and label for "info."
234     */
235    public void getTitleAndIcon(AppInfo application, LauncherActivityInfoCompat info,
236            HashMap<Object, CharSequence> labelCache) {
237        synchronized (mCache) {
238            CacheEntry entry = cacheLocked(application.componentName, info, labelCache,
239                    info.getUser());
240
241            application.title = entry.title;
242            application.iconBitmap = entry.icon;
243        }
244    }
245
246    public Bitmap getIcon(Intent intent, UserHandleCompat user) {
247        return getIcon(intent, null, user);
248    }
249
250    public Bitmap getIcon(Intent intent, String title, UserHandleCompat user) {
251        synchronized (mCache) {
252            final LauncherActivityInfoCompat launcherActInfo =
253                    mLauncherApps.resolveActivity(intent, user);
254            ComponentName component = intent.getComponent();
255
256            if (launcherActInfo == null || component == null) {
257                return getDefaultIcon(user);
258            }
259
260            CacheEntry entry = cacheLocked(component, launcherActInfo, null, user);
261            if (title != null) {
262                entry.title = title;
263            }
264            return entry.icon;
265        }
266    }
267
268    public Bitmap getDefaultIcon(UserHandleCompat user) {
269        if (!mDefaultIcons.containsKey(user)) {
270            mDefaultIcons.put(user, makeDefaultIcon(user));
271        }
272        return mDefaultIcons.get(user);
273    }
274
275    public Bitmap getIcon(ComponentName component, LauncherActivityInfoCompat info,
276            HashMap<Object, CharSequence> labelCache) {
277        synchronized (mCache) {
278            if (info == null || component == null) {
279                return null;
280            }
281
282            CacheEntry entry = cacheLocked(component, info, labelCache, info.getUser());
283            return entry.icon;
284        }
285    }
286
287    public boolean isDefaultIcon(Bitmap icon, UserHandleCompat user) {
288        return mDefaultIcons.get(user) == icon;
289    }
290
291    private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info,
292            HashMap<Object, CharSequence> labelCache, UserHandleCompat user) {
293        CacheKey cacheKey = new CacheKey(componentName, user);
294        CacheEntry entry = mCache.get(cacheKey);
295        if (entry == null) {
296            entry = new CacheEntry();
297
298            mCache.put(cacheKey, entry);
299
300            if (info != null) {
301                ComponentName labelKey = info.getComponentName();
302                if (labelCache != null && labelCache.containsKey(labelKey)) {
303                    entry.title = labelCache.get(labelKey).toString();
304                } else {
305                    entry.title = info.getLabel().toString();
306                    if (labelCache != null) {
307                        labelCache.put(labelKey, entry.title);
308                    }
309                }
310
311                entry.icon = Utilities.createIconBitmap(
312                        info.getBadgedIcon(mIconDpi), mContext);
313            } else {
314                entry.title = "";
315                Bitmap preloaded = getPreloadedIcon(componentName, user);
316                if (preloaded != null) {
317                    if (DEBUG) Log.d(TAG, "using preloaded icon for " +
318                            componentName.toShortString());
319                    entry.icon = preloaded;
320                } else {
321                    if (DEBUG) Log.d(TAG, "using default icon for " +
322                            componentName.toShortString());
323                    entry.icon = getDefaultIcon(user);
324                }
325            }
326        }
327        return entry;
328    }
329
330    public HashMap<ComponentName,Bitmap> getAllIcons() {
331        synchronized (mCache) {
332            HashMap<ComponentName,Bitmap> set = new HashMap<ComponentName,Bitmap>();
333            for (CacheKey ck : mCache.keySet()) {
334                final CacheEntry e = mCache.get(ck);
335                set.put(ck.componentName, e.icon);
336            }
337            return set;
338        }
339    }
340
341    /**
342     * Pre-load an icon into the persistent cache.
343     *
344     * <P>Queries for a component that does not exist in the package manager
345     * will be answered by the persistent cache.
346     *
347     * @param context application context
348     * @param componentName the icon should be returned for this component
349     * @param icon the icon to be persisted
350     * @param dpi the native density of the icon
351     */
352    public static void preloadIcon(Context context, ComponentName componentName, Bitmap icon,
353            int dpi) {
354        // TODO rescale to the correct native DPI
355        try {
356            PackageManager packageManager = context.getPackageManager();
357            packageManager.getActivityIcon(componentName);
358            // component is present on the system already, do nothing
359            return;
360        } catch (PackageManager.NameNotFoundException e) {
361            // pass
362        }
363
364        final String key = componentName.flattenToString();
365        FileOutputStream resourceFile = null;
366        try {
367            resourceFile = context.openFileOutput(getResourceFilename(componentName),
368                    Context.MODE_PRIVATE);
369            ByteArrayOutputStream os = new ByteArrayOutputStream();
370            if (icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 75, os)) {
371                byte[] buffer = os.toByteArray();
372                resourceFile.write(buffer, 0, buffer.length);
373            } else {
374                Log.w(TAG, "failed to encode cache for " + key);
375                return;
376            }
377        } catch (FileNotFoundException e) {
378            Log.w(TAG, "failed to pre-load cache for " + key, e);
379        } catch (IOException e) {
380            Log.w(TAG, "failed to pre-load cache for " + key, e);
381        } finally {
382            if (resourceFile != null) {
383                try {
384                    resourceFile.close();
385                } catch (IOException e) {
386                    Log.d(TAG, "failed to save restored icon for: " + key, e);
387                }
388            }
389        }
390    }
391
392    /**
393     * Read a pre-loaded icon from the persistent icon cache.
394     *
395     * @param componentName the component that should own the icon
396     * @returns a bitmap if one is cached, or null.
397     */
398    private Bitmap getPreloadedIcon(ComponentName componentName, UserHandleCompat user) {
399        final String key = componentName.flattenToShortString();
400
401        // We don't keep icons for other profiles in persistent cache.
402        if (!user.equals(UserHandleCompat.myUserHandle())) {
403            return null;
404        }
405
406        if (DEBUG) Log.v(TAG, "looking for pre-load icon for " + key);
407        Bitmap icon = null;
408        FileInputStream resourceFile = null;
409        try {
410            resourceFile = mContext.openFileInput(getResourceFilename(componentName));
411            byte[] buffer = new byte[1024];
412            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
413            int bytesRead = 0;
414            while(bytesRead >= 0) {
415                bytes.write(buffer, 0, bytesRead);
416                bytesRead = resourceFile.read(buffer, 0, buffer.length);
417            }
418            if (DEBUG) Log.d(TAG, "read " + bytes.size());
419            icon = BitmapFactory.decodeByteArray(bytes.toByteArray(), 0, bytes.size());
420            if (icon == null) {
421                Log.w(TAG, "failed to decode pre-load icon for " + key);
422            }
423        } catch (FileNotFoundException e) {
424            if (DEBUG) Log.d(TAG, "there is no restored icon for: " + key, e);
425        } catch (IOException e) {
426            Log.w(TAG, "failed to read pre-load icon for: " + key, e);
427        } finally {
428            if(resourceFile != null) {
429                try {
430                    resourceFile.close();
431                } catch (IOException e) {
432                    Log.d(TAG, "failed to manage pre-load icon file: " + key, e);
433                }
434            }
435        }
436
437        return icon;
438    }
439
440    /**
441     * Remove a pre-loaded icon from the persistent icon cache.
442     *
443     * @param componentName the component that should own the icon
444     * @returns true on success
445     */
446    public boolean deletePreloadedIcon(ComponentName componentName, UserHandleCompat user) {
447        // We don't keep icons for other profiles in persistent cache.
448        if (!user.equals(UserHandleCompat.myUserHandle())) {
449            return false;
450        }
451        if (componentName == null) {
452            return false;
453        }
454        if (mCache.remove(componentName) != null) {
455            if (DEBUG) Log.d(TAG, "removed pre-loaded icon from the in-memory cache");
456        }
457        boolean success = mContext.deleteFile(getResourceFilename(componentName));
458        if (DEBUG && success) Log.d(TAG, "removed pre-loaded icon from persistent cache");
459
460        return success;
461    }
462
463    private static String getResourceFilename(ComponentName component) {
464        String resourceName = component.flattenToShortString();
465        String filename = resourceName.replace(File.separatorChar, '_');
466        return RESOURCE_FILE_PREFIX + filename;
467    }
468}
469