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 android.content.ComponentName; 20import android.content.ContentValues; 21import android.content.Context; 22import android.content.Intent; 23import android.content.pm.ActivityInfo; 24import android.content.pm.ApplicationInfo; 25import android.content.pm.PackageInfo; 26import android.content.pm.PackageManager; 27import android.content.pm.PackageManager.NameNotFoundException; 28import android.content.res.Resources; 29import android.content.res.TypedArray; 30import android.database.Cursor; 31import android.database.sqlite.SQLiteDatabase; 32import android.database.sqlite.SQLiteException; 33import android.graphics.Bitmap; 34import android.graphics.BitmapFactory; 35import android.graphics.Canvas; 36import android.graphics.Color; 37import android.graphics.Paint; 38import android.graphics.Rect; 39import android.graphics.drawable.Drawable; 40import android.os.Handler; 41import android.os.SystemClock; 42import android.text.TextUtils; 43import android.util.Log; 44 45import com.android.launcher3.compat.LauncherActivityInfoCompat; 46import com.android.launcher3.compat.LauncherAppsCompat; 47import com.android.launcher3.compat.UserHandleCompat; 48import com.android.launcher3.compat.UserManagerCompat; 49import com.android.launcher3.config.FeatureFlags; 50import com.android.launcher3.model.PackageItemInfo; 51import com.android.launcher3.util.ComponentKey; 52import com.android.launcher3.util.SQLiteCacheHelper; 53import com.android.launcher3.util.Thunk; 54 55import java.util.Collections; 56import java.util.HashMap; 57import java.util.HashSet; 58import java.util.List; 59import java.util.Set; 60import java.util.Stack; 61 62/** 63 * Cache of application icons. Icons can be made from any thread. 64 */ 65public class IconCache { 66 67 private static final String TAG = "Launcher.IconCache"; 68 69 private static final int INITIAL_ICON_CACHE_CAPACITY = 50; 70 71 // Empty class name is used for storing package default entry. 72 private static final String EMPTY_CLASS_NAME = "."; 73 74 private static final boolean DEBUG = false; 75 private static final boolean DEBUG_IGNORE_CACHE = false; 76 77 private static final int LOW_RES_SCALE_FACTOR = 5; 78 79 @Thunk static final Object ICON_UPDATE_TOKEN = new Object(); 80 81 @Thunk static class CacheEntry { 82 public Bitmap icon; 83 public CharSequence title = ""; 84 public CharSequence contentDescription = ""; 85 public boolean isLowResIcon; 86 } 87 88 private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons = new HashMap<>(); 89 @Thunk final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); 90 91 private final Context mContext; 92 private final PackageManager mPackageManager; 93 private IconProvider mIconProvider; 94 @Thunk final UserManagerCompat mUserManager; 95 private final LauncherAppsCompat mLauncherApps; 96 private final HashMap<ComponentKey, CacheEntry> mCache = 97 new HashMap<ComponentKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY); 98 private final int mIconDpi; 99 @Thunk final IconDB mIconDb; 100 101 @Thunk final Handler mWorkerHandler; 102 103 // The background color used for activity icons. Since these icons are displayed in all-apps 104 // and folders, this would be same as the light quantum panel background. This color 105 // is used to convert icons to RGB_565. 106 private final int mActivityBgColor; 107 // The background color used for package icons. These are displayed in widget tray, which 108 // has a dark quantum panel background. 109 private final int mPackageBgColor; 110 private final BitmapFactory.Options mLowResOptions; 111 112 private Canvas mLowResCanvas; 113 private Paint mLowResPaint; 114 115 public IconCache(Context context, InvariantDeviceProfile inv) { 116 mContext = context; 117 mPackageManager = context.getPackageManager(); 118 mUserManager = UserManagerCompat.getInstance(mContext); 119 mLauncherApps = LauncherAppsCompat.getInstance(mContext); 120 mIconDpi = inv.fillResIconDpi; 121 mIconDb = new IconDB(context, inv.iconBitmapSize); 122 mLowResCanvas = new Canvas(); 123 mLowResPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); 124 125 mIconProvider = IconProvider.loadByName(context.getString(R.string.icon_provider_class), 126 context); 127 128 mWorkerHandler = new Handler(LauncherModel.getWorkerLooper()); 129 130 mActivityBgColor = context.getResources().getColor(R.color.quantum_panel_bg_color); 131 TypedArray ta = context.obtainStyledAttributes(new int[]{R.attr.colorSecondary}); 132 mPackageBgColor = ta.getColor(0, 0); 133 ta.recycle(); 134 mLowResOptions = new BitmapFactory.Options(); 135 // Always prefer RGB_565 config for low res. If the bitmap has transparency, it will 136 // automatically be loaded as ALPHA_8888. 137 mLowResOptions.inPreferredConfig = Bitmap.Config.RGB_565; 138 } 139 140 private Drawable getFullResDefaultActivityIcon() { 141 return getFullResIcon(Resources.getSystem(), android.R.mipmap.sym_def_app_icon); 142 } 143 144 private Drawable getFullResIcon(Resources resources, int iconId) { 145 Drawable d; 146 try { 147 d = resources.getDrawableForDensity(iconId, mIconDpi); 148 } catch (Resources.NotFoundException e) { 149 d = null; 150 } 151 152 return (d != null) ? d : getFullResDefaultActivityIcon(); 153 } 154 155 public Drawable getFullResIcon(String packageName, int iconId) { 156 Resources resources; 157 try { 158 resources = mPackageManager.getResourcesForApplication(packageName); 159 } catch (PackageManager.NameNotFoundException e) { 160 resources = null; 161 } 162 if (resources != null) { 163 if (iconId != 0) { 164 return getFullResIcon(resources, iconId); 165 } 166 } 167 return getFullResDefaultActivityIcon(); 168 } 169 170 public Drawable getFullResIcon(ActivityInfo info) { 171 Resources resources; 172 try { 173 resources = mPackageManager.getResourcesForApplication( 174 info.applicationInfo); 175 } catch (PackageManager.NameNotFoundException e) { 176 resources = null; 177 } 178 if (resources != null) { 179 int iconId = info.getIconResource(); 180 if (iconId != 0) { 181 return getFullResIcon(resources, iconId); 182 } 183 } 184 185 return getFullResDefaultActivityIcon(); 186 } 187 188 private Bitmap makeDefaultIcon(UserHandleCompat user) { 189 Drawable unbadged = getFullResDefaultActivityIcon(); 190 return Utilities.createBadgedIconBitmap(unbadged, user, mContext); 191 } 192 193 /** 194 * Remove any records for the supplied ComponentName. 195 */ 196 public synchronized void remove(ComponentName componentName, UserHandleCompat user) { 197 mCache.remove(new ComponentKey(componentName, user)); 198 } 199 200 /** 201 * Remove any records for the supplied package name from memory. 202 */ 203 private void removeFromMemCacheLocked(String packageName, UserHandleCompat user) { 204 HashSet<ComponentKey> forDeletion = new HashSet<ComponentKey>(); 205 for (ComponentKey key: mCache.keySet()) { 206 if (key.componentName.getPackageName().equals(packageName) 207 && key.user.equals(user)) { 208 forDeletion.add(key); 209 } 210 } 211 for (ComponentKey condemned: forDeletion) { 212 mCache.remove(condemned); 213 } 214 } 215 216 /** 217 * Updates the entries related to the given package in memory and persistent DB. 218 */ 219 public synchronized void updateIconsForPkg(String packageName, UserHandleCompat user) { 220 removeIconsForPkg(packageName, user); 221 try { 222 PackageInfo info = mPackageManager.getPackageInfo(packageName, 223 PackageManager.GET_UNINSTALLED_PACKAGES); 224 long userSerial = mUserManager.getSerialNumberForUser(user); 225 for (LauncherActivityInfoCompat app : mLauncherApps.getActivityList(packageName, user)) { 226 addIconToDBAndMemCache(app, info, userSerial); 227 } 228 } catch (NameNotFoundException e) { 229 Log.d(TAG, "Package not found", e); 230 return; 231 } 232 } 233 234 /** 235 * Removes the entries related to the given package in memory and persistent DB. 236 */ 237 public synchronized void removeIconsForPkg(String packageName, UserHandleCompat user) { 238 removeFromMemCacheLocked(packageName, user); 239 long userSerial = mUserManager.getSerialNumberForUser(user); 240 mIconDb.delete( 241 IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?", 242 new String[]{packageName + "/%", Long.toString(userSerial)}); 243 } 244 245 public void updateDbIcons(Set<String> ignorePackagesForMainUser) { 246 // Remove all active icon update tasks. 247 mWorkerHandler.removeCallbacksAndMessages(ICON_UPDATE_TOKEN); 248 249 mIconProvider.updateSystemStateString(); 250 for (UserHandleCompat user : mUserManager.getUserProfiles()) { 251 // Query for the set of apps 252 final List<LauncherActivityInfoCompat> apps = mLauncherApps.getActivityList(null, user); 253 // Fail if we don't have any apps 254 // TODO: Fix this. Only fail for the current user. 255 if (apps == null || apps.isEmpty()) { 256 return; 257 } 258 259 // Update icon cache. This happens in segments and {@link #onPackageIconsUpdated} 260 // is called by the icon cache when the job is complete. 261 updateDBIcons(user, apps, UserHandleCompat.myUserHandle().equals(user) 262 ? ignorePackagesForMainUser : Collections.<String>emptySet()); 263 } 264 } 265 266 /** 267 * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in 268 * the DB and are updated. 269 * @return The set of packages for which icons have updated. 270 */ 271 private void updateDBIcons(UserHandleCompat user, List<LauncherActivityInfoCompat> apps, 272 Set<String> ignorePackages) { 273 long userSerial = mUserManager.getSerialNumberForUser(user); 274 PackageManager pm = mContext.getPackageManager(); 275 HashMap<String, PackageInfo> pkgInfoMap = new HashMap<String, PackageInfo>(); 276 for (PackageInfo info : pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) { 277 pkgInfoMap.put(info.packageName, info); 278 } 279 280 HashMap<ComponentName, LauncherActivityInfoCompat> componentMap = new HashMap<>(); 281 for (LauncherActivityInfoCompat app : apps) { 282 componentMap.put(app.getComponentName(), app); 283 } 284 285 HashSet<Integer> itemsToRemove = new HashSet<Integer>(); 286 Stack<LauncherActivityInfoCompat> appsToUpdate = new Stack<>(); 287 288 Cursor c = null; 289 try { 290 c = mIconDb.query( 291 new String[]{IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT, 292 IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION, 293 IconDB.COLUMN_SYSTEM_STATE}, 294 IconDB.COLUMN_USER + " = ? ", 295 new String[]{Long.toString(userSerial)}); 296 297 final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT); 298 final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED); 299 final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION); 300 final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID); 301 final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE); 302 303 while (c.moveToNext()) { 304 String cn = c.getString(indexComponent); 305 ComponentName component = ComponentName.unflattenFromString(cn); 306 PackageInfo info = pkgInfoMap.get(component.getPackageName()); 307 if (info == null) { 308 if (!ignorePackages.contains(component.getPackageName())) { 309 remove(component, user); 310 itemsToRemove.add(c.getInt(rowIndex)); 311 } 312 continue; 313 } 314 if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) { 315 // Application is not present 316 continue; 317 } 318 319 long updateTime = c.getLong(indexLastUpdate); 320 int version = c.getInt(indexVersion); 321 LauncherActivityInfoCompat app = componentMap.remove(component); 322 if (version == info.versionCode && updateTime == info.lastUpdateTime && 323 TextUtils.equals(c.getString(systemStateIndex), 324 mIconProvider.getIconSystemState(info.packageName))) { 325 continue; 326 } 327 if (app == null) { 328 remove(component, user); 329 itemsToRemove.add(c.getInt(rowIndex)); 330 } else { 331 appsToUpdate.add(app); 332 } 333 } 334 } catch (SQLiteException e) { 335 Log.d(TAG, "Error reading icon cache", e); 336 // Continue updating whatever we have read so far 337 } finally { 338 if (c != null) { 339 c.close(); 340 } 341 } 342 if (!itemsToRemove.isEmpty()) { 343 mIconDb.delete( 344 Utilities.createDbSelectionQuery(IconDB.COLUMN_ROWID, itemsToRemove), null); 345 } 346 347 // Insert remaining apps. 348 if (!componentMap.isEmpty() || !appsToUpdate.isEmpty()) { 349 Stack<LauncherActivityInfoCompat> appsToAdd = new Stack<>(); 350 appsToAdd.addAll(componentMap.values()); 351 new SerializedIconUpdateTask(userSerial, pkgInfoMap, 352 appsToAdd, appsToUpdate).scheduleNext(); 353 } 354 } 355 356 @Thunk void addIconToDBAndMemCache(LauncherActivityInfoCompat app, PackageInfo info, 357 long userSerial) { 358 // Reuse the existing entry if it already exists in the DB. This ensures that we do not 359 // create bitmap if it was already created during loader. 360 ContentValues values = updateCacheAndGetContentValues(app, false); 361 addIconToDB(values, app.getComponentName(), info, userSerial); 362 } 363 364 /** 365 * Updates {@param values} to contain versoning information and adds it to the DB. 366 * @param values {@link ContentValues} containing icon & title 367 */ 368 private void addIconToDB(ContentValues values, ComponentName key, 369 PackageInfo info, long userSerial) { 370 values.put(IconDB.COLUMN_COMPONENT, key.flattenToString()); 371 values.put(IconDB.COLUMN_USER, userSerial); 372 values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime); 373 values.put(IconDB.COLUMN_VERSION, info.versionCode); 374 mIconDb.insertOrReplace(values); 375 } 376 377 @Thunk ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app, 378 boolean replaceExisting) { 379 final ComponentKey key = new ComponentKey(app.getComponentName(), app.getUser()); 380 CacheEntry entry = null; 381 if (!replaceExisting) { 382 entry = mCache.get(key); 383 // We can't reuse the entry if the high-res icon is not present. 384 if (entry == null || entry.isLowResIcon || entry.icon == null) { 385 entry = null; 386 } 387 } 388 if (entry == null) { 389 entry = new CacheEntry(); 390 entry.icon = Utilities.createBadgedIconBitmap( 391 mIconProvider.getIcon(app, mIconDpi), app.getUser(), 392 mContext); 393 } 394 entry.title = app.getLabel(); 395 entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, app.getUser()); 396 mCache.put(new ComponentKey(app.getComponentName(), app.getUser()), entry); 397 398 Bitmap lowResIcon = generateLowResIcon(entry.icon, mActivityBgColor); 399 return newContentValues(entry.icon, lowResIcon, entry.title.toString(), 400 app.getApplicationInfo().packageName); 401 } 402 403 /** 404 * Fetches high-res icon for the provided ItemInfo and updates the caller when done. 405 * @return a request ID that can be used to cancel the request. 406 */ 407 public IconLoadRequest updateIconInBackground(final BubbleTextView caller, final ItemInfo info) { 408 Runnable request = new Runnable() { 409 410 @Override 411 public void run() { 412 if (info instanceof AppInfo) { 413 getTitleAndIcon((AppInfo) info, null, false); 414 } else if (info instanceof ShortcutInfo) { 415 ShortcutInfo st = (ShortcutInfo) info; 416 getTitleAndIcon(st, 417 st.promisedIntent != null ? st.promisedIntent : st.intent, 418 st.user, false); 419 } else if (info instanceof PackageItemInfo) { 420 PackageItemInfo pti = (PackageItemInfo) info; 421 getTitleAndIconForApp(pti, false); 422 } 423 mMainThreadExecutor.execute(new Runnable() { 424 425 @Override 426 public void run() { 427 caller.reapplyItemInfo(info); 428 } 429 }); 430 } 431 }; 432 mWorkerHandler.post(request); 433 return new IconLoadRequest(request, mWorkerHandler); 434 } 435 436 private Bitmap getNonNullIcon(CacheEntry entry, UserHandleCompat user) { 437 return entry.icon == null ? getDefaultIcon(user) : entry.icon; 438 } 439 440 /** 441 * Fill in "application" with the icon and label for "info." 442 */ 443 public synchronized void getTitleAndIcon(AppInfo application, 444 LauncherActivityInfoCompat info, boolean useLowResIcon) { 445 UserHandleCompat user = info == null ? application.user : info.getUser(); 446 CacheEntry entry = cacheLocked(application.componentName, info, user, 447 false, useLowResIcon); 448 application.title = Utilities.trim(entry.title); 449 application.contentDescription = entry.contentDescription; 450 application.iconBitmap = getNonNullIcon(entry, user); 451 application.usingLowResIcon = entry.isLowResIcon; 452 } 453 454 /** 455 * Updates {@param application} only if a valid entry is found. 456 */ 457 public synchronized void updateTitleAndIcon(AppInfo application) { 458 CacheEntry entry = cacheLocked(application.componentName, null, application.user, 459 false, application.usingLowResIcon); 460 if (entry.icon != null && !isDefaultIcon(entry.icon, application.user)) { 461 application.title = Utilities.trim(entry.title); 462 application.contentDescription = entry.contentDescription; 463 application.iconBitmap = entry.icon; 464 application.usingLowResIcon = entry.isLowResIcon; 465 } 466 } 467 468 /** 469 * Returns a high res icon for the given intent and user 470 */ 471 public synchronized Bitmap getIcon(Intent intent, UserHandleCompat user) { 472 ComponentName component = intent.getComponent(); 473 // null info means not installed, but if we have a component from the intent then 474 // we should still look in the cache for restored app icons. 475 if (component == null) { 476 return getDefaultIcon(user); 477 } 478 479 LauncherActivityInfoCompat launcherActInfo = mLauncherApps.resolveActivity(intent, user); 480 CacheEntry entry = cacheLocked(component, launcherActInfo, user, true, false /* useLowRes */); 481 return entry.icon; 482 } 483 484 /** 485 * Fill in {@param shortcutInfo} with the icon and label for {@param intent}. If the 486 * corresponding activity is not found, it reverts to the package icon. 487 */ 488 public synchronized void getTitleAndIcon(ShortcutInfo shortcutInfo, Intent intent, 489 UserHandleCompat user, boolean useLowResIcon) { 490 ComponentName component = intent.getComponent(); 491 // null info means not installed, but if we have a component from the intent then 492 // we should still look in the cache for restored app icons. 493 if (component == null) { 494 shortcutInfo.setIcon(getDefaultIcon(user)); 495 shortcutInfo.title = ""; 496 shortcutInfo.contentDescription = ""; 497 shortcutInfo.usingFallbackIcon = true; 498 shortcutInfo.usingLowResIcon = false; 499 } else { 500 LauncherActivityInfoCompat info = mLauncherApps.resolveActivity(intent, user); 501 getTitleAndIcon(shortcutInfo, component, info, user, true, useLowResIcon); 502 } 503 } 504 505 /** 506 * Fill in {@param shortcutInfo} with the icon and label for {@param info} 507 */ 508 public synchronized void getTitleAndIcon( 509 ShortcutInfo shortcutInfo, ComponentName component, LauncherActivityInfoCompat info, 510 UserHandleCompat user, boolean usePkgIcon, boolean useLowResIcon) { 511 CacheEntry entry = cacheLocked(component, info, user, usePkgIcon, useLowResIcon); 512 shortcutInfo.setIcon(getNonNullIcon(entry, user)); 513 shortcutInfo.title = Utilities.trim(entry.title); 514 shortcutInfo.contentDescription = entry.contentDescription; 515 shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user); 516 shortcutInfo.usingLowResIcon = entry.isLowResIcon; 517 } 518 519 /** 520 * Fill in {@param infoInOut} with the corresponding icon and label. 521 */ 522 public synchronized void getTitleAndIconForApp( 523 PackageItemInfo infoInOut, boolean useLowResIcon) { 524 CacheEntry entry = getEntryForPackageLocked( 525 infoInOut.packageName, infoInOut.user, useLowResIcon); 526 infoInOut.title = Utilities.trim(entry.title); 527 infoInOut.contentDescription = entry.contentDescription; 528 infoInOut.iconBitmap = getNonNullIcon(entry, infoInOut.user); 529 infoInOut.usingLowResIcon = entry.isLowResIcon; 530 } 531 532 public synchronized Bitmap getDefaultIcon(UserHandleCompat user) { 533 if (!mDefaultIcons.containsKey(user)) { 534 mDefaultIcons.put(user, makeDefaultIcon(user)); 535 } 536 return mDefaultIcons.get(user); 537 } 538 539 public boolean isDefaultIcon(Bitmap icon, UserHandleCompat user) { 540 return mDefaultIcons.get(user) == icon; 541 } 542 543 /** 544 * Retrieves the entry from the cache. If the entry is not present, it creates a new entry. 545 * This method is not thread safe, it must be called from a synchronized method. 546 */ 547 private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info, 548 UserHandleCompat user, boolean usePackageIcon, boolean useLowResIcon) { 549 ComponentKey cacheKey = new ComponentKey(componentName, user); 550 CacheEntry entry = mCache.get(cacheKey); 551 if (entry == null || (entry.isLowResIcon && !useLowResIcon)) { 552 entry = new CacheEntry(); 553 mCache.put(cacheKey, entry); 554 555 // Check the DB first. 556 if (!getEntryFromDB(cacheKey, entry, useLowResIcon) || DEBUG_IGNORE_CACHE) { 557 if (info != null) { 558 entry.icon = Utilities.createBadgedIconBitmap( 559 mIconProvider.getIcon(info, mIconDpi), info.getUser(), 560 mContext); 561 } else { 562 if (usePackageIcon) { 563 CacheEntry packageEntry = getEntryForPackageLocked( 564 componentName.getPackageName(), user, false); 565 if (packageEntry != null) { 566 if (DEBUG) Log.d(TAG, "using package default icon for " + 567 componentName.toShortString()); 568 entry.icon = packageEntry.icon; 569 entry.title = packageEntry.title; 570 entry.contentDescription = packageEntry.contentDescription; 571 } 572 } 573 if (entry.icon == null) { 574 if (DEBUG) Log.d(TAG, "using default icon for " + 575 componentName.toShortString()); 576 entry.icon = getDefaultIcon(user); 577 } 578 } 579 } 580 581 if (TextUtils.isEmpty(entry.title) && info != null) { 582 entry.title = info.getLabel(); 583 entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); 584 } 585 } 586 return entry; 587 } 588 589 /** 590 * Adds a default package entry in the cache. This entry is not persisted and will be removed 591 * when the cache is flushed. 592 */ 593 public synchronized void cachePackageInstallInfo(String packageName, UserHandleCompat user, 594 Bitmap icon, CharSequence title) { 595 removeFromMemCacheLocked(packageName, user); 596 597 ComponentKey cacheKey = getPackageKey(packageName, user); 598 CacheEntry entry = mCache.get(cacheKey); 599 600 // For icon caching, do not go through DB. Just update the in-memory entry. 601 if (entry == null) { 602 entry = new CacheEntry(); 603 mCache.put(cacheKey, entry); 604 } 605 if (!TextUtils.isEmpty(title)) { 606 entry.title = title; 607 } 608 if (icon != null) { 609 entry.icon = Utilities.createIconBitmap(icon, mContext); 610 } 611 } 612 613 private static ComponentKey getPackageKey(String packageName, UserHandleCompat user) { 614 ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME); 615 return new ComponentKey(cn, user); 616 } 617 618 /** 619 * Gets an entry for the package, which can be used as a fallback entry for various components. 620 * This method is not thread safe, it must be called from a synchronized method. 621 */ 622 private CacheEntry getEntryForPackageLocked(String packageName, UserHandleCompat user, 623 boolean useLowResIcon) { 624 ComponentKey cacheKey = getPackageKey(packageName, user); 625 CacheEntry entry = mCache.get(cacheKey); 626 627 if (entry == null || (entry.isLowResIcon && !useLowResIcon)) { 628 entry = new CacheEntry(); 629 boolean entryUpdated = true; 630 631 // Check the DB first. 632 if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) { 633 try { 634 int flags = UserHandleCompat.myUserHandle().equals(user) ? 0 : 635 PackageManager.GET_UNINSTALLED_PACKAGES; 636 PackageInfo info = mPackageManager.getPackageInfo(packageName, flags); 637 ApplicationInfo appInfo = info.applicationInfo; 638 if (appInfo == null) { 639 throw new NameNotFoundException("ApplicationInfo is null"); 640 } 641 642 // Load the full res icon for the application, but if useLowResIcon is set, then 643 // only keep the low resolution icon instead of the larger full-sized icon 644 Bitmap icon = Utilities.createBadgedIconBitmap( 645 appInfo.loadIcon(mPackageManager), user, mContext); 646 Bitmap lowResIcon = generateLowResIcon(icon, mPackageBgColor); 647 entry.title = appInfo.loadLabel(mPackageManager); 648 entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); 649 entry.icon = useLowResIcon ? lowResIcon : icon; 650 entry.isLowResIcon = useLowResIcon; 651 652 // Add the icon in the DB here, since these do not get written during 653 // package updates. 654 ContentValues values = 655 newContentValues(icon, lowResIcon, entry.title.toString(), packageName); 656 addIconToDB(values, cacheKey.componentName, info, 657 mUserManager.getSerialNumberForUser(user)); 658 659 } catch (NameNotFoundException e) { 660 if (DEBUG) Log.d(TAG, "Application not installed " + packageName); 661 entryUpdated = false; 662 } 663 } 664 665 // Only add a filled-out entry to the cache 666 if (entryUpdated) { 667 mCache.put(cacheKey, entry); 668 } 669 } 670 return entry; 671 } 672 673 /** 674 * Pre-load an icon into the persistent cache. 675 * 676 * <P>Queries for a component that does not exist in the package manager 677 * will be answered by the persistent cache. 678 * 679 * @param componentName the icon should be returned for this component 680 * @param icon the icon to be persisted 681 * @param dpi the native density of the icon 682 */ 683 public void preloadIcon(ComponentName componentName, Bitmap icon, int dpi, String label, 684 long userSerial, InvariantDeviceProfile idp) { 685 // TODO rescale to the correct native DPI 686 try { 687 PackageManager packageManager = mContext.getPackageManager(); 688 packageManager.getActivityIcon(componentName); 689 // component is present on the system already, do nothing 690 return; 691 } catch (PackageManager.NameNotFoundException e) { 692 // pass 693 } 694 695 icon = Bitmap.createScaledBitmap(icon, idp.iconBitmapSize, idp.iconBitmapSize, true); 696 Bitmap lowResIcon = generateLowResIcon(icon, Color.TRANSPARENT); 697 ContentValues values = newContentValues(icon, lowResIcon, label, 698 componentName.getPackageName()); 699 values.put(IconDB.COLUMN_COMPONENT, componentName.flattenToString()); 700 values.put(IconDB.COLUMN_USER, userSerial); 701 mIconDb.insertOrReplace(values); 702 } 703 704 private boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) { 705 Cursor c = null; 706 try { 707 c = mIconDb.query( 708 new String[]{lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON, 709 IconDB.COLUMN_LABEL}, 710 IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", 711 new String[]{cacheKey.componentName.flattenToString(), 712 Long.toString(mUserManager.getSerialNumberForUser(cacheKey.user))}); 713 if (c.moveToNext()) { 714 entry.icon = loadIconNoResize(c, 0, lowRes ? mLowResOptions : null); 715 entry.isLowResIcon = lowRes; 716 entry.title = c.getString(1); 717 if (entry.title == null) { 718 entry.title = ""; 719 entry.contentDescription = ""; 720 } else { 721 entry.contentDescription = mUserManager.getBadgedLabelForUser( 722 entry.title, cacheKey.user); 723 } 724 return true; 725 } 726 } catch (SQLiteException e) { 727 Log.d(TAG, "Error reading icon cache", e); 728 } finally { 729 if (c != null) { 730 c.close(); 731 } 732 } 733 return false; 734 } 735 736 public static class IconLoadRequest { 737 private final Runnable mRunnable; 738 private final Handler mHandler; 739 740 IconLoadRequest(Runnable runnable, Handler handler) { 741 mRunnable = runnable; 742 mHandler = handler; 743 } 744 745 public void cancel() { 746 mHandler.removeCallbacks(mRunnable); 747 } 748 } 749 750 /** 751 * A runnable that updates invalid icons and adds missing icons in the DB for the provided 752 * LauncherActivityInfoCompat list. Items are updated/added one at a time, so that the 753 * worker thread doesn't get blocked. 754 */ 755 @Thunk class SerializedIconUpdateTask implements Runnable { 756 private final long mUserSerial; 757 private final HashMap<String, PackageInfo> mPkgInfoMap; 758 private final Stack<LauncherActivityInfoCompat> mAppsToAdd; 759 private final Stack<LauncherActivityInfoCompat> mAppsToUpdate; 760 private final HashSet<String> mUpdatedPackages = new HashSet<String>(); 761 762 @Thunk SerializedIconUpdateTask(long userSerial, HashMap<String, PackageInfo> pkgInfoMap, 763 Stack<LauncherActivityInfoCompat> appsToAdd, 764 Stack<LauncherActivityInfoCompat> appsToUpdate) { 765 mUserSerial = userSerial; 766 mPkgInfoMap = pkgInfoMap; 767 mAppsToAdd = appsToAdd; 768 mAppsToUpdate = appsToUpdate; 769 } 770 771 @Override 772 public void run() { 773 if (!mAppsToUpdate.isEmpty()) { 774 LauncherActivityInfoCompat app = mAppsToUpdate.pop(); 775 String pkg = app.getComponentName().getPackageName(); 776 PackageInfo info = mPkgInfoMap.get(pkg); 777 if (info != null) { 778 synchronized (IconCache.this) { 779 ContentValues values = updateCacheAndGetContentValues(app, true); 780 addIconToDB(values, app.getComponentName(), info, mUserSerial); 781 } 782 mUpdatedPackages.add(pkg); 783 } 784 if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) { 785 // No more app to update. Notify model. 786 LauncherAppState.getInstance().getModel().onPackageIconsUpdated( 787 mUpdatedPackages, mUserManager.getUserForSerialNumber(mUserSerial)); 788 } 789 790 // Let it run one more time. 791 scheduleNext(); 792 } else if (!mAppsToAdd.isEmpty()) { 793 LauncherActivityInfoCompat app = mAppsToAdd.pop(); 794 PackageInfo info = mPkgInfoMap.get(app.getComponentName().getPackageName()); 795 if (info != null) { 796 synchronized (IconCache.this) { 797 addIconToDBAndMemCache(app, info, mUserSerial); 798 } 799 } 800 801 if (!mAppsToAdd.isEmpty()) { 802 scheduleNext(); 803 } 804 } 805 } 806 807 public void scheduleNext() { 808 mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN, SystemClock.uptimeMillis() + 1); 809 } 810 } 811 812 private static final class IconDB extends SQLiteCacheHelper { 813 private final static int DB_VERSION = 10; 814 815 private final static int RELEASE_VERSION = DB_VERSION + 816 (FeatureFlags.LAUNCHER3_DISABLE_ICON_NORMALIZATION ? 0 : 1); 817 818 private final static String TABLE_NAME = "icons"; 819 private final static String COLUMN_ROWID = "rowid"; 820 private final static String COLUMN_COMPONENT = "componentName"; 821 private final static String COLUMN_USER = "profileId"; 822 private final static String COLUMN_LAST_UPDATED = "lastUpdated"; 823 private final static String COLUMN_VERSION = "version"; 824 private final static String COLUMN_ICON = "icon"; 825 private final static String COLUMN_ICON_LOW_RES = "icon_low_res"; 826 private final static String COLUMN_LABEL = "label"; 827 private final static String COLUMN_SYSTEM_STATE = "system_state"; 828 829 public IconDB(Context context, int iconPixelSize) { 830 super(context, LauncherFiles.APP_ICONS_DB, 831 (RELEASE_VERSION << 16) + iconPixelSize, 832 TABLE_NAME); 833 } 834 835 @Override 836 protected void onCreateTable(SQLiteDatabase db) { 837 db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + 838 COLUMN_COMPONENT + " TEXT NOT NULL, " + 839 COLUMN_USER + " INTEGER NOT NULL, " + 840 COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " + 841 COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " + 842 COLUMN_ICON + " BLOB, " + 843 COLUMN_ICON_LOW_RES + " BLOB, " + 844 COLUMN_LABEL + " TEXT, " + 845 COLUMN_SYSTEM_STATE + " TEXT, " + 846 "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " + 847 ");"); 848 } 849 } 850 851 private ContentValues newContentValues(Bitmap icon, Bitmap lowResIcon, String label, 852 String packageName) { 853 ContentValues values = new ContentValues(); 854 values.put(IconDB.COLUMN_ICON, Utilities.flattenBitmap(icon)); 855 values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap(lowResIcon)); 856 857 values.put(IconDB.COLUMN_LABEL, label); 858 values.put(IconDB.COLUMN_SYSTEM_STATE, mIconProvider.getIconSystemState(packageName)); 859 860 return values; 861 } 862 863 /** 864 * Generates a new low-res icon given a high-res icon. 865 */ 866 private Bitmap generateLowResIcon(Bitmap icon, int lowResBackgroundColor) { 867 if (lowResBackgroundColor == Color.TRANSPARENT) { 868 return Bitmap.createScaledBitmap(icon, 869 icon.getWidth() / LOW_RES_SCALE_FACTOR, 870 icon.getHeight() / LOW_RES_SCALE_FACTOR, true); 871 } else { 872 Bitmap lowResIcon = Bitmap.createBitmap(icon.getWidth() / LOW_RES_SCALE_FACTOR, 873 icon.getHeight() / LOW_RES_SCALE_FACTOR, Bitmap.Config.RGB_565); 874 synchronized (this) { 875 mLowResCanvas.setBitmap(lowResIcon); 876 mLowResCanvas.drawColor(lowResBackgroundColor); 877 mLowResCanvas.drawBitmap(icon, new Rect(0, 0, icon.getWidth(), icon.getHeight()), 878 new Rect(0, 0, lowResIcon.getWidth(), lowResIcon.getHeight()), 879 mLowResPaint); 880 mLowResCanvas.setBitmap(null); 881 } 882 return lowResIcon; 883 } 884 } 885 886 private static Bitmap loadIconNoResize(Cursor c, int iconIndex, BitmapFactory.Options options) { 887 byte[] data = c.getBlob(iconIndex); 888 try { 889 return BitmapFactory.decodeByteArray(data, 0, data.length, options); 890 } catch (Exception e) { 891 return null; 892 } 893 } 894} 895