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