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