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