blob: 5a0875b3055163b6d306d0d66a473a566fd1d2de [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.Log;
import com.android.launcher3.compat.LauncherActivityInfoCompat;
import com.android.launcher3.compat.LauncherAppsCompat;
import com.android.launcher3.compat.UserHandleCompat;
import com.android.launcher3.compat.UserManagerCompat;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map.Entry;
/**
* Cache of application icons. Icons can be made from any thread.
*/
public class IconCache {
private static final String TAG = "Launcher.IconCache";
private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
private static final String RESOURCE_FILE_PREFIX = "icon_";
// Empty class name is used for storing package default entry.
private static final String EMPTY_CLASS_NAME = ".";
private static final boolean DEBUG = false;
private static class CacheEntry {
public Bitmap icon;
public CharSequence title;
public CharSequence contentDescription;
}
private static class CacheKey {
public ComponentName componentName;
public UserHandleCompat user;
CacheKey(ComponentName componentName, UserHandleCompat user) {
this.componentName = componentName;
this.user = user;
}
@Override
public int hashCode() {
return componentName.hashCode() + user.hashCode();
}
@Override
public boolean equals(Object o) {
CacheKey other = (CacheKey) o;
return other.componentName.equals(componentName) && other.user.equals(user);
}
}
private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons =
new HashMap<UserHandleCompat, Bitmap>();
private final Context mContext;
private final PackageManager mPackageManager;
private final UserManagerCompat mUserManager;
private final LauncherAppsCompat mLauncherApps;
private final HashMap<CacheKey, CacheEntry> mCache =
new HashMap<CacheKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY);
private int mIconDpi;
public IconCache(Context context) {
ActivityManager activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
mContext = context;
mPackageManager = context.getPackageManager();
mUserManager = UserManagerCompat.getInstance(mContext);
mLauncherApps = LauncherAppsCompat.getInstance(mContext);
mIconDpi = activityManager.getLauncherLargeIconDensity();
// need to set mIconDpi before getting default icon
UserHandleCompat myUser = UserHandleCompat.myUserHandle();
mDefaultIcons.put(myUser, makeDefaultIcon(myUser));
}
public Drawable getFullResDefaultActivityIcon() {
return getFullResIcon(Resources.getSystem(), android.R.mipmap.sym_def_app_icon);
}
private Drawable getFullResIcon(Resources resources, int iconId) {
Drawable d;
try {
d = resources.getDrawableForDensity(iconId, mIconDpi);
} catch (Resources.NotFoundException e) {
d = null;
}
return (d != null) ? d : getFullResDefaultActivityIcon();
}
public Drawable getFullResIcon(String packageName, int iconId) {
Resources resources;
try {
resources = mPackageManager.getResourcesForApplication(packageName);
} catch (PackageManager.NameNotFoundException e) {
resources = null;
}
if (resources != null) {
if (iconId != 0) {
return getFullResIcon(resources, iconId);
}
}
return getFullResDefaultActivityIcon();
}
public int getFullResIconDpi() {
return mIconDpi;
}
public Drawable getFullResIcon(ActivityInfo info) {
Resources resources;
try {
resources = mPackageManager.getResourcesForApplication(
info.applicationInfo);
} catch (PackageManager.NameNotFoundException e) {
resources = null;
}
if (resources != null) {
int iconId = info.getIconResource();
if (iconId != 0) {
return getFullResIcon(resources, iconId);
}
}
return getFullResDefaultActivityIcon();
}
private Bitmap makeDefaultIcon(UserHandleCompat user) {
Drawable unbadged = getFullResDefaultActivityIcon();
Drawable d = mUserManager.getBadgedDrawableForUser(unbadged, user);
Bitmap b = Bitmap.createBitmap(Math.max(d.getIntrinsicWidth(), 1),
Math.max(d.getIntrinsicHeight(), 1),
Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
d.setBounds(0, 0, b.getWidth(), b.getHeight());
d.draw(c);
c.setBitmap(null);
return b;
}
/**
* Remove any records for the supplied ComponentName.
*/
public synchronized void remove(ComponentName componentName, UserHandleCompat user) {
mCache.remove(new CacheKey(componentName, user));
}
/**
* Remove any records for the supplied package name.
*/
public synchronized void remove(String packageName, UserHandleCompat user) {
HashSet<CacheKey> forDeletion = new HashSet<CacheKey>();
for (CacheKey key: mCache.keySet()) {
if (key.componentName.getPackageName().equals(packageName)
&& key.user.equals(user)) {
forDeletion.add(key);
}
}
for (CacheKey condemned: forDeletion) {
mCache.remove(condemned);
}
}
/**
* Empty out the cache.
*/
public synchronized void flush() {
mCache.clear();
}
/**
* Empty out the cache that aren't of the correct grid size
*/
public synchronized void flushInvalidIcons(DeviceProfile grid) {
Iterator<Entry<CacheKey, CacheEntry>> it = mCache.entrySet().iterator();
while (it.hasNext()) {
final CacheEntry e = it.next().getValue();
if ((e.icon != null) && (e.icon.getWidth() < grid.iconSizePx
|| e.icon.getHeight() < grid.iconSizePx)) {
it.remove();
}
}
}
/**
* Fill in "application" with the icon and label for "info."
*/
public synchronized void getTitleAndIcon(AppInfo application, LauncherActivityInfoCompat info,
HashMap<Object, CharSequence> labelCache) {
CacheEntry entry = cacheLocked(application.componentName, info, labelCache,
info.getUser(), false);
application.title = entry.title;
application.iconBitmap = entry.icon;
application.contentDescription = entry.contentDescription;
}
public synchronized Bitmap getIcon(Intent intent, UserHandleCompat user) {
ComponentName component = intent.getComponent();
// null info means not installed, but if we have a component from the intent then
// we should still look in the cache for restored app icons.
if (component == null) {
return getDefaultIcon(user);
}
LauncherActivityInfoCompat launcherActInfo = mLauncherApps.resolveActivity(intent, user);
CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, true);
return entry.icon;
}
/**
* Fill in "shortcutInfo" with the icon and label for "info."
*/
public synchronized void getTitleAndIcon(ShortcutInfo shortcutInfo, Intent intent,
UserHandleCompat user, boolean usePkgIcon) {
ComponentName component = intent.getComponent();
// null info means not installed, but if we have a component from the intent then
// we should still look in the cache for restored app icons.
if (component == null) {
shortcutInfo.setIcon(getDefaultIcon(user));
shortcutInfo.title = "";
shortcutInfo.usingFallbackIcon = true;
} else {
LauncherActivityInfoCompat launcherActInfo =
mLauncherApps.resolveActivity(intent, user);
CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, usePkgIcon);
shortcutInfo.setIcon(entry.icon);
shortcutInfo.title = entry.title;
shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user);
}
}
public synchronized Bitmap getDefaultIcon(UserHandleCompat user) {
if (!mDefaultIcons.containsKey(user)) {
mDefaultIcons.put(user, makeDefaultIcon(user));
}
return mDefaultIcons.get(user);
}
public synchronized Bitmap getIcon(ComponentName component, LauncherActivityInfoCompat info,
HashMap<Object, CharSequence> labelCache) {
if (info == null || component == null) {
return null;
}
CacheEntry entry = cacheLocked(component, info, labelCache, info.getUser(), false);
return entry.icon;
}
public boolean isDefaultIcon(Bitmap icon, UserHandleCompat user) {
return mDefaultIcons.get(user) == icon;
}
/**
* Retrieves the entry from the cache. If the entry is not present, it creates a new entry.
* This method is not thread safe, it must be called from a synchronized method.
*/
private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info,
HashMap<Object, CharSequence> labelCache, UserHandleCompat user, boolean usePackageIcon) {
CacheKey cacheKey = new CacheKey(componentName, user);
CacheEntry entry = mCache.get(cacheKey);
if (entry == null) {
entry = new CacheEntry();
mCache.put(cacheKey, entry);
if (info != null) {
ComponentName labelKey = info.getComponentName();
if (labelCache != null && labelCache.containsKey(labelKey)) {
entry.title = labelCache.get(labelKey).toString();
} else {
entry.title = info.getLabel().toString();
if (labelCache != null) {
labelCache.put(labelKey, entry.title);
}
}
entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user);
entry.icon = Utilities.createIconBitmap(
info.getBadgedIcon(mIconDpi), mContext);
} else {
entry.title = "";
Bitmap preloaded = getPreloadedIcon(componentName, user);
if (preloaded != null) {
if (DEBUG) Log.d(TAG, "using preloaded icon for " +
componentName.toShortString());
entry.icon = preloaded;
} else {
if (usePackageIcon) {
CacheEntry packageEntry = getEntryForPackage(
componentName.getPackageName(), user);
if (packageEntry != null) {
if (DEBUG) Log.d(TAG, "using package default icon for " +
componentName.toShortString());
entry.icon = packageEntry.icon;
entry.title = packageEntry.title;
}
}
if (entry.icon == null) {
if (DEBUG) Log.d(TAG, "using default icon for " +
componentName.toShortString());
entry.icon = getDefaultIcon(user);
}
}
}
}
return entry;
}
/**
* Adds a default package entry in the cache. This entry is not persisted and will be removed
* when the cache is flushed.
*/
public synchronized void cachePackageInstallInfo(String packageName, UserHandleCompat user,
Bitmap icon, CharSequence title) {
remove(packageName, user);
CacheEntry entry = getEntryForPackage(packageName, user);
if (!TextUtils.isEmpty(title)) {
entry.title = title;
}
if (icon != null) {
entry.icon = Utilities.createIconBitmap(icon, mContext);
}
}
/**
* Gets an entry for the package, which can be used as a fallback entry for various components.
* This method is not thread safe, it must be called from a synchronized method.
*/
private CacheEntry getEntryForPackage(String packageName, UserHandleCompat user) {
ComponentName cn = new ComponentName(packageName, EMPTY_CLASS_NAME);;
CacheKey cacheKey = new CacheKey(cn, user);
CacheEntry entry = mCache.get(cacheKey);
if (entry == null) {
entry = new CacheEntry();
entry.title = "";
mCache.put(cacheKey, entry);
try {
ApplicationInfo info = mPackageManager.getApplicationInfo(packageName, 0);
entry.title = info.loadLabel(mPackageManager);
entry.icon = Utilities.createIconBitmap(info.loadIcon(mPackageManager), mContext);
} catch (NameNotFoundException e) {
if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
}
if (entry.icon == null) {
entry.icon = getPreloadedIcon(cn, user);
}
}
return entry;
}
public synchronized HashMap<ComponentName,Bitmap> getAllIcons() {
HashMap<ComponentName,Bitmap> set = new HashMap<ComponentName,Bitmap>();
for (CacheKey ck : mCache.keySet()) {
final CacheEntry e = mCache.get(ck);
set.put(ck.componentName, e.icon);
}
return set;
}
/**
* Pre-load an icon into the persistent cache.
*
* <P>Queries for a component that does not exist in the package manager
* will be answered by the persistent cache.
*
* @param context application context
* @param componentName the icon should be returned for this component
* @param icon the icon to be persisted
* @param dpi the native density of the icon
*/
public static void preloadIcon(Context context, ComponentName componentName, Bitmap icon,
int dpi) {
// TODO rescale to the correct native DPI
try {
PackageManager packageManager = context.getPackageManager();
packageManager.getActivityIcon(componentName);
// component is present on the system already, do nothing
return;
} catch (PackageManager.NameNotFoundException e) {
// pass
}
final String key = componentName.flattenToString();
FileOutputStream resourceFile = null;
try {
resourceFile = context.openFileOutput(getResourceFilename(componentName),
Context.MODE_PRIVATE);
ByteArrayOutputStream os = new ByteArrayOutputStream();
if (icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 75, os)) {
byte[] buffer = os.toByteArray();
resourceFile.write(buffer, 0, buffer.length);
} else {
Log.w(TAG, "failed to encode cache for " + key);
return;
}
} catch (FileNotFoundException e) {
Log.w(TAG, "failed to pre-load cache for " + key, e);
} catch (IOException e) {
Log.w(TAG, "failed to pre-load cache for " + key, e);
} finally {
if (resourceFile != null) {
try {
resourceFile.close();
} catch (IOException e) {
Log.d(TAG, "failed to save restored icon for: " + key, e);
}
}
}
}
/**
* Read a pre-loaded icon from the persistent icon cache.
*
* @param componentName the component that should own the icon
* @returns a bitmap if one is cached, or null.
*/
private Bitmap getPreloadedIcon(ComponentName componentName, UserHandleCompat user) {
final String key = componentName.flattenToShortString();
// We don't keep icons for other profiles in persistent cache.
if (!user.equals(UserHandleCompat.myUserHandle())) {
return null;
}
if (DEBUG) Log.v(TAG, "looking for pre-load icon for " + key);
Bitmap icon = null;
FileInputStream resourceFile = null;
try {
resourceFile = mContext.openFileInput(getResourceFilename(componentName));
byte[] buffer = new byte[1024];
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
int bytesRead = 0;
while(bytesRead >= 0) {
bytes.write(buffer, 0, bytesRead);
bytesRead = resourceFile.read(buffer, 0, buffer.length);
}
if (DEBUG) Log.d(TAG, "read " + bytes.size());
icon = BitmapFactory.decodeByteArray(bytes.toByteArray(), 0, bytes.size());
if (icon == null) {
Log.w(TAG, "failed to decode pre-load icon for " + key);
}
} catch (FileNotFoundException e) {
if (DEBUG) Log.d(TAG, "there is no restored icon for: " + key);
} catch (IOException e) {
Log.w(TAG, "failed to read pre-load icon for: " + key, e);
} finally {
if(resourceFile != null) {
try {
resourceFile.close();
} catch (IOException e) {
Log.d(TAG, "failed to manage pre-load icon file: " + key, e);
}
}
}
return icon;
}
/**
* Remove a pre-loaded icon from the persistent icon cache.
*
* @param componentName the component that should own the icon
*/
public void deletePreloadedIcon(ComponentName componentName, UserHandleCompat user) {
// We don't keep icons for other profiles in persistent cache.
if (!user.equals(UserHandleCompat.myUserHandle()) || componentName == null) {
return;
}
remove(componentName, user);
boolean success = mContext.deleteFile(getResourceFilename(componentName));
if (DEBUG && success) Log.d(TAG, "removed pre-loaded icon from persistent cache");
}
private static String getResourceFilename(ComponentName component) {
String resourceName = component.flattenToShortString();
String filename = resourceName.replace(File.separatorChar, '_');
return RESOURCE_FILE_PREFIX + filename;
}
}