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 com.android.launcher3.backup.BackupProtos; 20 21 import android.app.ActivityManager; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.ActivityInfo; 26 import android.content.pm.PackageManager; 27 import android.content.pm.ResolveInfo; 28 import android.content.res.Resources; 29 import android.graphics.Bitmap; 30 import android.graphics.BitmapFactory; 31 import android.graphics.Canvas; 32 import android.graphics.Paint; 33 import android.graphics.drawable.Drawable; 34 import android.util.Log; 35 36 import java.io.ByteArrayOutputStream; 37 import java.io.File; 38 import java.io.FileInputStream; 39 import java.io.FileNotFoundException; 40 import java.io.FileOutputStream; 41 import java.io.IOException; 42 import java.util.HashMap; 43 import java.util.HashSet; 44 import java.util.Iterator; 45 import java.util.Map.Entry; 46 47 /** 48 * Cache of application icons. Icons can be made from any thread. 49 */ 50 public class IconCache { 51 @SuppressWarnings("unused") 52 private static final String TAG = "Launcher.IconCache"; 53 54 private static final int INITIAL_ICON_CACHE_CAPACITY = 50; 55 private static final String RESOURCE_FILE_PREFIX = "icon_"; 56 57 private static final boolean DEBUG = true; 58 59 private static class CacheEntry { 60 public Bitmap icon; 61 public String title; 62 } 63 64 private final Bitmap mDefaultIcon; 65 private final Context mContext; 66 private final PackageManager mPackageManager; 67 private final HashMap<ComponentName, CacheEntry> mCache = 68 new HashMap<ComponentName, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY); 69 private int mIconDpi; 70 71 public IconCache(Context context) { 72 ActivityManager activityManager = 73 (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 74 75 mContext = context; 76 mPackageManager = context.getPackageManager(); 77 mIconDpi = activityManager.getLauncherLargeIconDensity(); 78 79 // need to set mIconDpi before getting default icon 80 mDefaultIcon = makeDefaultIcon(); 81 } 82 83 public Drawable getFullResDefaultActivityIcon() { 84 return getFullResIcon(Resources.getSystem(), 85 android.R.mipmap.sym_def_app_icon); 86 } 87 88 public Drawable getFullResIcon(Resources resources, int iconId) { 89 Drawable d; 90 try { 91 d = resources.getDrawableForDensity(iconId, mIconDpi); 92 } catch (Resources.NotFoundException e) { 93 d = null; 94 } 95 96 return (d != null) ? d : getFullResDefaultActivityIcon(); 97 } 98 99 public Drawable getFullResIcon(String packageName, int iconId) { 100 Resources resources; 101 try { 102 resources = mPackageManager.getResourcesForApplication(packageName); 103 } catch (PackageManager.NameNotFoundException e) { 104 resources = null; 105 } 106 if (resources != null) { 107 if (iconId != 0) { 108 return getFullResIcon(resources, iconId); 109 } 110 } 111 return getFullResDefaultActivityIcon(); 112 } 113 114 public Drawable getFullResIcon(ResolveInfo info) { 115 return getFullResIcon(info.activityInfo); 116 } 117 118 public Drawable getFullResIcon(ActivityInfo info) { 119 120 Resources resources; 121 try { 122 resources = mPackageManager.getResourcesForApplication( 123 info.applicationInfo); 124 } catch (PackageManager.NameNotFoundException e) { 125 resources = null; 126 } 127 if (resources != null) { 128 int iconId = info.getIconResource(); 129 if (iconId != 0) { 130 return getFullResIcon(resources, iconId); 131 } 132 } 133 134 return getFullResDefaultActivityIcon(); 135 } 136 137 private Bitmap makeDefaultIcon() { 138 Drawable d = getFullResDefaultActivityIcon(); 139 Bitmap b = Bitmap.createBitmap(Math.max(d.getIntrinsicWidth(), 1), 140 Math.max(d.getIntrinsicHeight(), 1), 141 Bitmap.Config.ARGB_8888); 142 Canvas c = new Canvas(b); 143 d.setBounds(0, 0, b.getWidth(), b.getHeight()); 144 d.draw(c); 145 c.setBitmap(null); 146 return b; 147 } 148 149 /** 150 * Remove any records for the supplied ComponentName. 151 */ 152 public void remove(ComponentName componentName) { 153 synchronized (mCache) { 154 mCache.remove(componentName); 155 } 156 } 157 158 /** 159 * Remove any records for the supplied package name. 160 */ 161 public void remove(String packageName) { 162 HashSet<ComponentName> forDeletion = new HashSet<ComponentName>(); 163 for (ComponentName componentName: mCache.keySet()) { 164 if (componentName.getPackageName().equals(packageName)) { 165 forDeletion.add(componentName); 166 } 167 } 168 for (ComponentName condemned: forDeletion) { 169 remove(condemned); 170 } 171 } 172 173 /** 174 * Empty out the cache. 175 */ 176 public void flush() { 177 synchronized (mCache) { 178 mCache.clear(); 179 } 180 } 181 182 /** 183 * Empty out the cache that aren't of the correct grid size 184 */ 185 public void flushInvalidIcons(DeviceProfile grid) { 186 synchronized (mCache) { 187 Iterator<Entry<ComponentName, CacheEntry>> it = mCache.entrySet().iterator(); 188 while (it.hasNext()) { 189 final CacheEntry e = it.next().getValue(); 190 if (e.icon.getWidth() < grid.iconSizePx || e.icon.getHeight() < grid.iconSizePx) { 191 it.remove(); 192 } 193 } 194 } 195 } 196 197 /** 198 * Fill in "application" with the icon and label for "info." 199 */ 200 public void getTitleAndIcon(AppInfo application, ResolveInfo info, 201 HashMap<Object, CharSequence> labelCache) { 202 synchronized (mCache) { 203 CacheEntry entry = cacheLocked(application.componentName, info, labelCache); 204 205 application.title = entry.title; 206 application.iconBitmap = entry.icon; 207 } 208 } 209 210 public Bitmap getIcon(Intent intent) { 211 return getIcon(intent, null); 212 } 213 214 public Bitmap getIcon(Intent intent, String title) { 215 synchronized (mCache) { 216 final ResolveInfo resolveInfo = mPackageManager.resolveActivity(intent, 0); 217 ComponentName component = intent.getComponent(); 218 219 if (component == null) { 220 return mDefaultIcon; 221 } 222 223 CacheEntry entry = cacheLocked(component, resolveInfo, null); 224 if (title != null) { 225 entry.title = title; 226 } 227 return entry.icon; 228 } 229 } 230 231 public Bitmap getIcon(ComponentName component, ResolveInfo resolveInfo, 232 HashMap<Object, CharSequence> labelCache) { 233 synchronized (mCache) { 234 if (resolveInfo == null || component == null) { 235 return null; 236 } 237 238 CacheEntry entry = cacheLocked(component, resolveInfo, labelCache); 239 return entry.icon; 240 } 241 } 242 243 public boolean isDefaultIcon(Bitmap icon) { 244 return mDefaultIcon == icon; 245 } 246 247 private CacheEntry cacheLocked(ComponentName componentName, ResolveInfo info, 248 HashMap<Object, CharSequence> labelCache) { 249 CacheEntry entry = mCache.get(componentName); 250 if (entry == null) { 251 entry = new CacheEntry(); 252 253 mCache.put(componentName, entry); 254 255 if (info != null) { 256 ComponentName key = LauncherModel.getComponentNameFromResolveInfo(info); 257 if (labelCache != null && labelCache.containsKey(key)) { 258 entry.title = labelCache.get(key).toString(); 259 } else { 260 entry.title = info.loadLabel(mPackageManager).toString(); 261 if (labelCache != null) { 262 labelCache.put(key, entry.title); 263 } 264 } 265 if (entry.title == null) { 266 entry.title = info.activityInfo.name; 267 } 268 269 entry.icon = Utilities.createIconBitmap( 270 getFullResIcon(info), mContext); 271 } else { 272 entry.title = ""; 273 Bitmap preloaded = getPreloadedIcon(componentName); 274 if (preloaded != null) { 275 if (DEBUG) Log.d(TAG, "using preloaded icon for " + 276 componentName.toShortString()); 277 entry.icon = preloaded; 278 } else { 279 if (DEBUG) Log.d(TAG, "using default icon for " + 280 componentName.toShortString()); 281 entry.icon = mDefaultIcon; 282 } 283 } 284 } 285 return entry; 286 } 287 288 public HashMap<ComponentName,Bitmap> getAllIcons() { 289 synchronized (mCache) { 290 HashMap<ComponentName,Bitmap> set = new HashMap<ComponentName,Bitmap>(); 291 for (ComponentName cn : mCache.keySet()) { 292 final CacheEntry e = mCache.get(cn); 293 set.put(cn, e.icon); 294 } 295 return set; 296 } 297 } 298 299 /** 300 * Pre-load an icon into the persistent cache. 301 * 302 * <P>Queries for a component that does not exist in the package manager 303 * will be answered by the persistent cache. 304 * 305 * @param context application context 306 * @param componentName the icon should be returned for this component 307 * @param icon the icon to be persisted 308 * @param dpi the native density of the icon 309 */ 310 public static void preloadIcon(Context context, ComponentName componentName, Bitmap icon, 311 int dpi) { 312 // TODO rescale to the correct native DPI 313 try { 314 PackageManager packageManager = context.getPackageManager(); 315 packageManager.getActivityIcon(componentName); 316 // component is present on the system already, do nothing 317 return; 318 } catch (PackageManager.NameNotFoundException e) { 319 // pass 320 } 321 322 final String key = componentName.flattenToString(); 323 FileOutputStream resourceFile = null; 324 try { 325 resourceFile = context.openFileOutput(getResourceFilename(componentName), 326 Context.MODE_PRIVATE); 327 ByteArrayOutputStream os = new ByteArrayOutputStream(); 328 if (icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 75, os)) { 329 byte[] buffer = os.toByteArray(); 330 resourceFile.write(buffer, 0, buffer.length); 331 } else { 332 Log.w(TAG, "failed to encode cache for " + key); 333 return; 334 } 335 } catch (FileNotFoundException e) { 336 Log.w(TAG, "failed to pre-load cache for " + key, e); 337 } catch (IOException e) { 338 Log.w(TAG, "failed to pre-load cache for " + key, e); 339 } finally { 340 if (resourceFile != null) { 341 try { 342 resourceFile.close(); 343 } catch (IOException e) { 344 Log.d(TAG, "failed to save restored icon for: " + key, e); 345 } 346 } 347 } 348 } 349 350 /** 351 * Read a pre-loaded icon from the persistent icon cache. 352 * 353 * @param componentName the component that should own the icon 354 * @returns a bitmap if one is cached, or null. 355 */ 356 private Bitmap getPreloadedIcon(ComponentName componentName) { 357 final String key = componentName.flattenToShortString(); 358 359 if (DEBUG) Log.v(TAG, "looking for pre-load icon for " + key); 360 Bitmap icon = null; 361 FileInputStream resourceFile = null; 362 try { 363 resourceFile = mContext.openFileInput(getResourceFilename(componentName)); 364 byte[] buffer = new byte[1024]; 365 ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 366 int bytesRead = 0; 367 while(bytesRead >= 0) { 368 bytes.write(buffer, 0, bytesRead); 369 bytesRead = resourceFile.read(buffer, 0, buffer.length); 370 } 371 if (DEBUG) Log.d(TAG, "read " + bytes.size()); 372 icon = BitmapFactory.decodeByteArray(bytes.toByteArray(), 0, bytes.size()); 373 if (icon == null) { 374 Log.w(TAG, "failed to decode pre-load icon for " + key); 375 } 376 } catch (FileNotFoundException e) { 377 if (DEBUG) Log.d(TAG, "there is no restored icon for: " + key, e); 378 } catch (IOException e) { 379 Log.w(TAG, "failed to read pre-load icon for: " + key, e); 380 } finally { 381 if(resourceFile != null) { 382 try { 383 resourceFile.close(); 384 } catch (IOException e) { 385 Log.d(TAG, "failed to manage pre-load icon file: " + key, e); 386 } 387 } 388 } 389 390 if (icon != null) { 391 // TODO: handle alpha mask in the view layer 392 Bitmap b = Bitmap.createBitmap(Math.max(icon.getWidth(), 1), 393 Math.max(icon.getHeight(), 1), 394 Bitmap.Config.ARGB_8888); 395 Canvas c = new Canvas(b); 396 Paint paint = new Paint(); 397 paint.setAlpha(127); 398 c.drawBitmap(icon, 0, 0, paint); 399 c.setBitmap(null); 400 icon.recycle(); 401 icon = b; 402 } 403 404 return icon; 405 } 406 407 /** 408 * Remove a pre-loaded icon from the persistent icon cache. 409 * 410 * @param componentName the component that should own the icon 411 * @returns true on success 412 */ 413 public boolean deletePreloadedIcon(ComponentName componentName) { 414 if (componentName == null) { 415 return false; 416 } 417 if (mCache.remove(componentName) != null) { 418 if (DEBUG) Log.d(TAG, "removed pre-loaded icon from the in-memory cache"); 419 } 420 boolean success = mContext.deleteFile(getResourceFilename(componentName)); 421 if (DEBUG && success) Log.d(TAG, "removed pre-loaded icon from persistent cache"); 422 423 return success; 424 } 425 426 private static String getResourceFilename(ComponentName component) { 427 String resourceName = component.flattenToShortString(); 428 String filename = resourceName.replace(File.separatorChar, '_'); 429 return RESOURCE_FILE_PREFIX + filename; 430 } 431 } 432