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