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