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