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