1 /* 2 * Copyright (C) 2018 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 package com.android.launcher3.icons.cache; 17 18 import static android.graphics.BitmapFactory.decodeByteArray; 19 20 import static com.android.launcher3.icons.BaseIconFactory.getFullResDefaultActivityIcon; 21 import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON; 22 import static com.android.launcher3.icons.GraphicsUtils.flattenBitmap; 23 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; 24 25 import static java.util.Objects.requireNonNull; 26 27 import android.content.ComponentName; 28 import android.content.ContentValues; 29 import android.content.Context; 30 import android.content.pm.ActivityInfo; 31 import android.content.pm.ApplicationInfo; 32 import android.content.pm.PackageInfo; 33 import android.content.pm.PackageManager; 34 import android.content.pm.PackageManager.NameNotFoundException; 35 import android.content.res.Resources; 36 import android.database.Cursor; 37 import android.database.sqlite.SQLiteDatabase; 38 import android.database.sqlite.SQLiteException; 39 import android.graphics.Bitmap; 40 import android.graphics.Bitmap.Config; 41 import android.graphics.BitmapFactory; 42 import android.graphics.drawable.Drawable; 43 import android.os.Build; 44 import android.os.Handler; 45 import android.os.LocaleList; 46 import android.os.Looper; 47 import android.os.Process; 48 import android.os.Trace; 49 import android.os.UserHandle; 50 import android.text.TextUtils; 51 import android.util.Log; 52 import android.util.SparseArray; 53 54 import androidx.annotation.NonNull; 55 import androidx.annotation.Nullable; 56 import androidx.annotation.VisibleForTesting; 57 import androidx.annotation.WorkerThread; 58 59 import com.android.launcher3.icons.BaseIconFactory; 60 import com.android.launcher3.icons.BaseIconFactory.IconOptions; 61 import com.android.launcher3.icons.BitmapInfo; 62 import com.android.launcher3.util.ComponentKey; 63 import com.android.launcher3.util.FlagOp; 64 import com.android.launcher3.util.SQLiteCacheHelper; 65 66 import java.nio.ByteBuffer; 67 import java.util.AbstractMap; 68 import java.util.Arrays; 69 import java.util.Collections; 70 import java.util.HashMap; 71 import java.util.HashSet; 72 import java.util.Map; 73 import java.util.Set; 74 import java.util.function.Supplier; 75 76 public abstract class BaseIconCache { 77 78 private static final String TAG = "BaseIconCache"; 79 private static final boolean DEBUG = false; 80 81 private static final int INITIAL_ICON_CACHE_CAPACITY = 50; 82 // A format string which returns the original string as is. 83 private static final String IDENTITY_FORMAT_STRING = "%1$s"; 84 85 // Empty class name is used for storing package default entry. 86 public static final String EMPTY_CLASS_NAME = "."; 87 88 public static class CacheEntry { 89 90 @NonNull 91 public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO; 92 @NonNull 93 public CharSequence title = ""; 94 @NonNull 95 public CharSequence contentDescription = ""; 96 } 97 98 @NonNull 99 protected final Context mContext; 100 101 @NonNull 102 protected final PackageManager mPackageManager; 103 104 @NonNull 105 private final Map<ComponentKey, CacheEntry> mCache; 106 107 @NonNull 108 protected final Handler mWorkerHandler; 109 110 protected int mIconDpi; 111 112 @NonNull 113 protected IconDB mIconDb; 114 115 @NonNull 116 protected LocaleList mLocaleList = LocaleList.getEmptyLocaleList(); 117 118 @NonNull 119 protected String mSystemState = ""; 120 121 @Nullable 122 private BitmapInfo mDefaultIcon; 123 124 @NonNull 125 private final SparseArray<FlagOp> mUserFlagOpMap = new SparseArray<>(); 126 127 private final SparseArray<String> mUserFormatString = new SparseArray<>(); 128 129 @Nullable 130 private final String mDbFileName; 131 132 @NonNull 133 private final Looper mBgLooper; 134 135 private volatile boolean mIconUpdateInProgress = false; 136 BaseIconCache(@onNull final Context context, @Nullable final String dbFileName, @NonNull final Looper bgLooper, final int iconDpi, final int iconPixelSize, final boolean inMemoryCache)137 public BaseIconCache(@NonNull final Context context, @Nullable final String dbFileName, 138 @NonNull final Looper bgLooper, final int iconDpi, final int iconPixelSize, 139 final boolean inMemoryCache) { 140 mContext = context; 141 mDbFileName = dbFileName; 142 mPackageManager = context.getPackageManager(); 143 mBgLooper = bgLooper; 144 mWorkerHandler = new Handler(mBgLooper); 145 146 if (inMemoryCache) { 147 mCache = new HashMap<>(INITIAL_ICON_CACHE_CAPACITY); 148 } else { 149 // Use a dummy cache 150 mCache = new AbstractMap<ComponentKey, CacheEntry>() { 151 @Override 152 public Set<Entry<ComponentKey, CacheEntry>> entrySet() { 153 return Collections.emptySet(); 154 } 155 156 @Override 157 public CacheEntry put(ComponentKey key, CacheEntry value) { 158 return value; 159 } 160 }; 161 } 162 163 updateSystemState(); 164 mIconDpi = iconDpi; 165 mIconDb = new IconDB(context, dbFileName, iconPixelSize); 166 } 167 168 /** 169 * Returns the persistable serial number for {@param user}. Subclass should implement proper 170 * caching strategy to avoid making binder call every time. 171 */ getSerialNumberForUser(@onNull final UserHandle user)172 protected abstract long getSerialNumberForUser(@NonNull final UserHandle user); 173 174 /** 175 * Return true if the given app is an instant app and should be badged appropriately. 176 */ isInstantApp(@onNull final ApplicationInfo info)177 protected abstract boolean isInstantApp(@NonNull final ApplicationInfo info); 178 179 /** 180 * Opens and returns an icon factory. The factory is recycled by the caller. 181 */ 182 @NonNull getIconFactory()183 public abstract BaseIconFactory getIconFactory(); 184 updateIconParams(final int iconDpi, final int iconPixelSize)185 public void updateIconParams(final int iconDpi, final int iconPixelSize) { 186 mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize)); 187 } 188 updateIconParamsBg(final int iconDpi, final int iconPixelSize)189 private synchronized void updateIconParamsBg(final int iconDpi, final int iconPixelSize) { 190 mIconDpi = iconDpi; 191 mDefaultIcon = null; 192 mUserFlagOpMap.clear(); 193 mIconDb.clear(); 194 mIconDb.close(); 195 mIconDb = new IconDB(mContext, mDbFileName, iconPixelSize); 196 mCache.clear(); 197 } 198 199 @Nullable getFullResIcon(@ullable final Resources resources, final int iconId)200 private Drawable getFullResIcon(@Nullable final Resources resources, final int iconId) { 201 if (resources != null && iconId != 0) { 202 try { 203 return resources.getDrawableForDensity(iconId, mIconDpi); 204 } catch (Resources.NotFoundException e) { 205 } 206 } 207 return getFullResDefaultActivityIcon(mIconDpi); 208 } 209 210 @Nullable getFullResIcon(@onNull final String packageName, final int iconId)211 public Drawable getFullResIcon(@NonNull final String packageName, final int iconId) { 212 try { 213 return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId); 214 } catch (PackageManager.NameNotFoundException e) { 215 } 216 return getFullResDefaultActivityIcon(mIconDpi); 217 } 218 219 @Nullable getFullResIcon(@onNull final ActivityInfo info)220 public Drawable getFullResIcon(@NonNull final ActivityInfo info) { 221 try { 222 return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo), 223 info.getIconResource()); 224 } catch (PackageManager.NameNotFoundException e) { 225 } 226 return getFullResDefaultActivityIcon(mIconDpi); 227 } 228 setIconUpdateInProgress(boolean updating)229 public void setIconUpdateInProgress(boolean updating) { 230 mIconUpdateInProgress = updating; 231 } 232 isIconUpdateInProgress()233 public boolean isIconUpdateInProgress() { 234 return mIconUpdateInProgress; 235 } 236 237 /** 238 * Remove any records for the supplied ComponentName. 239 */ remove(@onNull final ComponentName componentName, @NonNull final UserHandle user)240 public synchronized void remove(@NonNull final ComponentName componentName, 241 @NonNull final UserHandle user) { 242 mCache.remove(new ComponentKey(componentName, user)); 243 } 244 245 /** 246 * Remove any records for the supplied package name from memory. 247 */ removeFromMemCacheLocked(@ullable final String packageName, @Nullable final UserHandle user)248 private void removeFromMemCacheLocked(@Nullable final String packageName, 249 @Nullable final UserHandle user) { 250 HashSet<ComponentKey> forDeletion = new HashSet<>(); 251 for (ComponentKey key : mCache.keySet()) { 252 if (key.componentName.getPackageName().equals(packageName) 253 && key.user.equals(user)) { 254 forDeletion.add(key); 255 } 256 } 257 for (ComponentKey condemned : forDeletion) { 258 mCache.remove(condemned); 259 } 260 } 261 262 /** 263 * Removes the entries related to the given package in memory and persistent DB. 264 */ removeIconsForPkg(@onNull final String packageName, @NonNull final UserHandle user)265 public synchronized void removeIconsForPkg(@NonNull final String packageName, 266 @NonNull final UserHandle user) { 267 removeFromMemCacheLocked(packageName, user); 268 long userSerial = getSerialNumberForUser(user); 269 mIconDb.delete( 270 IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?", 271 new String[]{packageName + "/%", Long.toString(userSerial)}); 272 } 273 274 @NonNull getUpdateHandler()275 public IconCacheUpdateHandler getUpdateHandler() { 276 updateSystemState(); 277 return new IconCacheUpdateHandler(this); 278 } 279 280 /** 281 * Refreshes the system state definition used to check the validity of the cache. It 282 * incorporates all the properties that can affect the cache like the list of enabled locale 283 * and system-version. 284 */ updateSystemState()285 private void updateSystemState() { 286 mLocaleList = mContext.getResources().getConfiguration().getLocales(); 287 mSystemState = mLocaleList.toLanguageTags() + "," + Build.VERSION.SDK_INT; 288 mUserFormatString.clear(); 289 } 290 291 @NonNull getIconSystemState(@ullable final String packageName)292 protected String getIconSystemState(@Nullable final String packageName) { 293 return mSystemState; 294 } 295 getUserBadgedLabel(CharSequence label, UserHandle user)296 public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) { 297 int key = user.hashCode(); 298 int index = mUserFormatString.indexOfKey(key); 299 String format; 300 if (index < 0) { 301 format = mPackageManager.getUserBadgedLabel(IDENTITY_FORMAT_STRING, user).toString(); 302 if (TextUtils.equals(IDENTITY_FORMAT_STRING, format)) { 303 format = null; 304 } 305 mUserFormatString.put(key, format); 306 } else { 307 format = mUserFormatString.valueAt(index); 308 } 309 return format == null ? label : String.format(format, label); 310 } 311 312 /** 313 * Adds an entry into the DB and the in-memory cache. 314 * 315 * @param replaceExisting if true, it will recreate the bitmap even if it already exists in 316 * the memory. This is useful then the previous bitmap was created using 317 * old data. 318 */ 319 @VisibleForTesting addIconToDBAndMemCache(@onNull final T object, @NonNull final CachingLogic<T> cachingLogic, @NonNull final PackageInfo info, final long userSerial, final boolean replaceExisting)320 public synchronized <T> void addIconToDBAndMemCache(@NonNull final T object, 321 @NonNull final CachingLogic<T> cachingLogic, @NonNull final PackageInfo info, 322 final long userSerial, final boolean replaceExisting) { 323 UserHandle user = cachingLogic.getUser(object); 324 ComponentName componentName = cachingLogic.getComponent(object); 325 326 final ComponentKey key = new ComponentKey(componentName, user); 327 CacheEntry entry = null; 328 if (!replaceExisting) { 329 entry = mCache.get(key); 330 // We can't reuse the entry if the high-res icon is not present. 331 if (entry == null || entry.bitmap.isNullOrLowRes()) { 332 entry = null; 333 } 334 } 335 if (entry == null) { 336 entry = new CacheEntry(); 337 entry.bitmap = cachingLogic.loadIcon(mContext, object); 338 } 339 // Icon can't be loaded from cachingLogic, which implies alternative icon was loaded 340 // (e.g. fallback icon, default icon). So we drop here since there's no point in caching 341 // an empty entry. 342 if (entry.bitmap.isNullOrLowRes()) return; 343 344 CharSequence entryTitle = cachingLogic.getLabel(object); 345 if (TextUtils.isEmpty(entryTitle)) { 346 if (entryTitle == null) { 347 Log.wtf(TAG, "No label returned from caching logic instance: " + cachingLogic); 348 } 349 entryTitle = componentName.getPackageName(); 350 } 351 entry.title = entryTitle; 352 353 entry.contentDescription = getUserBadgedLabel(entry.title, user); 354 if (cachingLogic.addToMemCache()) mCache.put(key, entry); 355 356 ContentValues values = newContentValues(entry.bitmap, entry.title.toString(), 357 componentName.getPackageName(), cachingLogic.getKeywords(object, mLocaleList)); 358 addIconToDB(values, componentName, info, userSerial, 359 cachingLogic.getLastUpdatedTime(object, info)); 360 } 361 362 /** 363 * Updates {@param values} to contain versioning information and adds it to the DB. 364 * 365 * @param values {@link ContentValues} containing icon & title 366 */ addIconToDB(@onNull final ContentValues values, @NonNull final ComponentName key, @NonNull final PackageInfo info, final long userSerial, final long lastUpdateTime)367 private void addIconToDB(@NonNull final ContentValues values, @NonNull final ComponentName key, 368 @NonNull final PackageInfo info, final long userSerial, final long lastUpdateTime) { 369 values.put(IconDB.COLUMN_COMPONENT, key.flattenToString()); 370 values.put(IconDB.COLUMN_USER, userSerial); 371 values.put(IconDB.COLUMN_LAST_UPDATED, lastUpdateTime); 372 values.put(IconDB.COLUMN_VERSION, info.versionCode); 373 mIconDb.insertOrReplace(values); 374 } 375 376 @NonNull getDefaultIcon(@onNull final UserHandle user)377 public synchronized BitmapInfo getDefaultIcon(@NonNull final UserHandle user) { 378 if (mDefaultIcon == null) { 379 try (BaseIconFactory li = getIconFactory()) { 380 mDefaultIcon = li.makeDefaultIcon(); 381 } 382 } 383 return mDefaultIcon.withFlags(getUserFlagOpLocked(user)); 384 } 385 386 @NonNull getUserFlagOpLocked(@onNull final UserHandle user)387 protected FlagOp getUserFlagOpLocked(@NonNull final UserHandle user) { 388 int key = user.hashCode(); 389 int index; 390 if ((index = mUserFlagOpMap.indexOfKey(key)) >= 0) { 391 return mUserFlagOpMap.valueAt(index); 392 } else { 393 try (BaseIconFactory li = getIconFactory()) { 394 FlagOp op = li.getBitmapFlagOp(new IconOptions().setUser(user)); 395 mUserFlagOpMap.put(key, op); 396 return op; 397 } 398 } 399 } 400 isDefaultIcon(@onNull final BitmapInfo icon, @NonNull final UserHandle user)401 public boolean isDefaultIcon(@NonNull final BitmapInfo icon, @NonNull final UserHandle user) { 402 return getDefaultIcon(user).icon == icon.icon; 403 } 404 405 /** 406 * Retrieves the entry from the cache. If the entry is not present, it creates a new entry. 407 * This method is not thread safe, it must be called from a synchronized method. 408 */ 409 @NonNull cacheLocked( @onNull final ComponentName componentName, @NonNull final UserHandle user, @NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic, final boolean usePackageIcon, final boolean useLowResIcon)410 protected <T> CacheEntry cacheLocked( 411 @NonNull final ComponentName componentName, @NonNull final UserHandle user, 412 @NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic, 413 final boolean usePackageIcon, final boolean useLowResIcon) { 414 return cacheLocked( 415 componentName, 416 user, 417 infoProvider, 418 cachingLogic, 419 null, 420 usePackageIcon, 421 useLowResIcon); 422 } 423 424 @NonNull cacheLocked( @onNull final ComponentName componentName, @NonNull final UserHandle user, @NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic, @Nullable final Cursor cursor, final boolean usePackageIcon, final boolean useLowResIcon)425 protected <T> CacheEntry cacheLocked( 426 @NonNull final ComponentName componentName, @NonNull final UserHandle user, 427 @NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic, 428 @Nullable final Cursor cursor, final boolean usePackageIcon, 429 final boolean useLowResIcon) { 430 assertWorkerThread(); 431 ComponentKey cacheKey = new ComponentKey(componentName, user); 432 CacheEntry entry = mCache.get(cacheKey); 433 if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) { 434 entry = new CacheEntry(); 435 if (cachingLogic.addToMemCache()) { 436 mCache.put(cacheKey, entry); 437 } 438 439 // Check the DB first. 440 T object = null; 441 boolean providerFetchedOnce = false; 442 boolean cacheEntryUpdated = cursor == null 443 ? getEntryFromDBLocked(cacheKey, entry, useLowResIcon) 444 : updateTitleAndIconLocked(cacheKey, entry, cursor, useLowResIcon); 445 if (!cacheEntryUpdated) { 446 object = infoProvider.get(); 447 providerFetchedOnce = true; 448 449 loadFallbackIcon( 450 object, 451 entry, 452 cachingLogic, 453 usePackageIcon, 454 /* usePackageTitle= */ true, 455 componentName, 456 user); 457 } 458 459 if (TextUtils.isEmpty(entry.title)) { 460 if (object == null && !providerFetchedOnce) { 461 object = infoProvider.get(); 462 providerFetchedOnce = true; 463 } 464 if (object != null) { 465 loadFallbackTitle(object, entry, cachingLogic, user); 466 } 467 } 468 } 469 return entry; 470 } 471 472 /** 473 * Fallback method for loading an icon bitmap. 474 */ loadFallbackIcon(@ullable final T object, @NonNull final CacheEntry entry, @NonNull final CachingLogic<T> cachingLogic, final boolean usePackageIcon, final boolean usePackageTitle, @NonNull final ComponentName componentName, @NonNull final UserHandle user)475 protected <T> void loadFallbackIcon(@Nullable final T object, @NonNull final CacheEntry entry, 476 @NonNull final CachingLogic<T> cachingLogic, final boolean usePackageIcon, 477 final boolean usePackageTitle, @NonNull final ComponentName componentName, 478 @NonNull final UserHandle user) { 479 if (object != null) { 480 entry.bitmap = cachingLogic.loadIcon(mContext, object); 481 } else { 482 if (usePackageIcon) { 483 CacheEntry packageEntry = getEntryForPackageLocked( 484 componentName.getPackageName(), user, false); 485 if (DEBUG) { 486 Log.d(TAG, "using package default icon for " 487 + componentName.toShortString()); 488 } 489 entry.bitmap = packageEntry.bitmap; 490 entry.contentDescription = packageEntry.contentDescription; 491 492 if (usePackageTitle) { 493 entry.title = packageEntry.title; 494 } 495 } 496 if (entry.bitmap == null) { 497 // TODO: entry.bitmap can never be null, so this should not happen at all. 498 Log.wtf(TAG, "using default icon for " + componentName.toShortString()); 499 entry.bitmap = getDefaultIcon(user); 500 } 501 } 502 } 503 504 /** 505 * Fallback method for loading an app title. 506 */ loadFallbackTitle( @onNull final T object, @NonNull final CacheEntry entry, @NonNull final CachingLogic<T> cachingLogic, @NonNull final UserHandle user)507 protected <T> void loadFallbackTitle( 508 @NonNull final T object, @NonNull final CacheEntry entry, 509 @NonNull final CachingLogic<T> cachingLogic, @NonNull final UserHandle user) { 510 entry.title = cachingLogic.getLabel(object); 511 if (TextUtils.isEmpty(entry.title)) { 512 entry.title = cachingLogic.getComponent(object).getPackageName(); 513 } 514 entry.contentDescription = getUserBadgedLabel( 515 cachingLogic.getDescription(object, entry.title), user); 516 } 517 clearMemoryCache()518 public synchronized void clearMemoryCache() { 519 assertWorkerThread(); 520 mCache.clear(); 521 } 522 523 /** 524 * Adds a default package entry in the cache. This entry is not persisted and will be removed 525 * when the cache is flushed. 526 */ cachePackageInstallInfo(@onNull final String packageName, @NonNull final UserHandle user, @Nullable final Bitmap icon, @Nullable final CharSequence title)527 protected synchronized void cachePackageInstallInfo(@NonNull final String packageName, 528 @NonNull final UserHandle user, @Nullable final Bitmap icon, 529 @Nullable final CharSequence title) { 530 removeFromMemCacheLocked(packageName, user); 531 532 ComponentKey cacheKey = getPackageKey(packageName, user); 533 CacheEntry entry = mCache.get(cacheKey); 534 535 // For icon caching, do not go through DB. Just update the in-memory entry. 536 if (entry == null) { 537 entry = new CacheEntry(); 538 } 539 if (!TextUtils.isEmpty(title)) { 540 entry.title = title; 541 } 542 if (icon != null) { 543 BaseIconFactory li = getIconFactory(); 544 entry.bitmap = li.createShapedIconBitmap(icon, new IconOptions().setUser(user)); 545 li.close(); 546 } 547 if (!TextUtils.isEmpty(title) && entry.bitmap.icon != null) { 548 mCache.put(cacheKey, entry); 549 } 550 } 551 552 @NonNull getPackageKey(@onNull final String packageName, @NonNull final UserHandle user)553 private static ComponentKey getPackageKey(@NonNull final String packageName, 554 @NonNull final UserHandle user) { 555 ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME); 556 return new ComponentKey(cn, user); 557 } 558 559 /** 560 * Gets an entry for the package, which can be used as a fallback entry for various components. 561 * This method is not thread safe, it must be called from a synchronized method. 562 */ 563 @WorkerThread 564 @NonNull 565 @SuppressWarnings("NewApi") getEntryForPackageLocked(@onNull final String packageName, @NonNull final UserHandle user, final boolean useLowResIcon)566 protected CacheEntry getEntryForPackageLocked(@NonNull final String packageName, 567 @NonNull final UserHandle user, final boolean useLowResIcon) { 568 assertWorkerThread(); 569 ComponentKey cacheKey = getPackageKey(packageName, user); 570 CacheEntry entry = mCache.get(cacheKey); 571 572 if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) { 573 entry = new CacheEntry(); 574 boolean entryUpdated = true; 575 576 // Check the DB first. 577 if (!getEntryFromDBLocked(cacheKey, entry, useLowResIcon)) { 578 try { 579 long flags = Process.myUserHandle().equals(user) ? 0 : 580 PackageManager.GET_UNINSTALLED_PACKAGES; 581 flags |= PackageManager.MATCH_ARCHIVED_PACKAGES; 582 PackageInfo info = mPackageManager.getPackageInfo(packageName, 583 PackageManager.PackageInfoFlags.of(flags)); 584 ApplicationInfo appInfo = info.applicationInfo; 585 if (appInfo == null) { 586 NameNotFoundException e = new NameNotFoundException( 587 "ApplicationInfo is null"); 588 logdPersistently(TAG, 589 String.format("ApplicationInfo is null for %s", packageName), 590 e); 591 throw e; 592 } 593 594 BaseIconFactory li = getIconFactory(); 595 // Load the full res icon for the application, but if useLowResIcon is set, then 596 // only keep the low resolution icon instead of the larger full-sized icon 597 Drawable appIcon = appInfo.loadIcon(mPackageManager); 598 if (mPackageManager.isDefaultApplicationIcon(appIcon)) { 599 logdPersistently(TAG, 600 String.format("Default icon returned for %s", packageName), 601 null); 602 } 603 BitmapInfo iconInfo = li.createBadgedIconBitmap(appIcon, 604 new IconOptions().setUser(user).setInstantApp(isInstantApp(appInfo))); 605 li.close(); 606 607 entry.title = appInfo.loadLabel(mPackageManager); 608 entry.contentDescription = getUserBadgedLabel(entry.title, user); 609 entry.bitmap = useLowResIcon 610 ? BitmapInfo.of(LOW_RES_ICON, iconInfo.color) 611 : iconInfo; 612 613 // Add the icon in the DB here, since these do not get written during 614 // package updates. 615 ContentValues values = newContentValues( 616 iconInfo, entry.title.toString(), packageName, null); 617 addIconToDB(values, cacheKey.componentName, info, getSerialNumberForUser(user), 618 info.lastUpdateTime); 619 620 } catch (NameNotFoundException e) { 621 if (DEBUG) Log.d(TAG, "Application not installed " + packageName); 622 entryUpdated = false; 623 } 624 } 625 626 // Only add a filled-out entry to the cache 627 if (entryUpdated) { 628 mCache.put(cacheKey, entry); 629 } 630 } 631 return entry; 632 } 633 getEntryFromDBLocked(@onNull final ComponentKey cacheKey, @NonNull final CacheEntry entry, final boolean lowRes)634 protected boolean getEntryFromDBLocked(@NonNull final ComponentKey cacheKey, 635 @NonNull final CacheEntry entry, final boolean lowRes) { 636 Cursor c = null; 637 Trace.beginSection("loadIconIndividually"); 638 try { 639 c = mIconDb.query( 640 lowRes ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES, 641 IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", 642 new String[]{ 643 cacheKey.componentName.flattenToString(), 644 Long.toString(getSerialNumberForUser(cacheKey.user))}); 645 if (c.moveToNext()) { 646 return updateTitleAndIconLocked(cacheKey, entry, c, lowRes); 647 } 648 } catch (SQLiteException e) { 649 Log.d(TAG, "Error reading icon cache", e); 650 } finally { 651 if (c != null) { 652 c.close(); 653 } 654 Trace.endSection(); 655 } 656 return false; 657 } 658 updateTitleAndIconLocked( @onNull final ComponentKey cacheKey, @NonNull final CacheEntry entry, @NonNull final Cursor c, final boolean lowRes)659 private boolean updateTitleAndIconLocked( 660 @NonNull final ComponentKey cacheKey, @NonNull final CacheEntry entry, 661 @NonNull final Cursor c, final boolean lowRes) { 662 // Set the alpha to be 255, so that we never have a wrong color 663 entry.bitmap = BitmapInfo.of(LOW_RES_ICON, 664 setColorAlphaBound(c.getInt(IconDB.INDEX_COLOR), 255)); 665 entry.title = c.getString(IconDB.INDEX_TITLE); 666 if (entry.title == null) { 667 entry.title = ""; 668 entry.contentDescription = ""; 669 } else { 670 entry.contentDescription = getUserBadgedLabel(entry.title, cacheKey.user); 671 } 672 673 if (!lowRes) { 674 byte[] data = c.getBlob(IconDB.INDEX_ICON); 675 if (data == null) { 676 return false; 677 } 678 try { 679 BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); 680 decodeOptions.inPreferredConfig = Config.HARDWARE; 681 entry.bitmap = BitmapInfo.of( 682 requireNonNull(decodeByteArray(data, 0, data.length, decodeOptions)), 683 entry.bitmap.color); 684 } catch (Exception e) { 685 return false; 686 } 687 688 // Decode mono bitmap 689 data = c.getBlob(IconDB.INDEX_MONO_ICON); 690 Bitmap icon = entry.bitmap.icon; 691 if (data != null && data.length == icon.getHeight() * icon.getWidth()) { 692 Bitmap monoBitmap = Bitmap.createBitmap( 693 icon.getWidth(), icon.getHeight(), Config.ALPHA_8); 694 monoBitmap.copyPixelsFromBuffer(ByteBuffer.wrap(data)); 695 Bitmap hwMonoBitmap = monoBitmap.copy(Config.HARDWARE, false /*isMutable*/); 696 if (hwMonoBitmap != null) { 697 monoBitmap.recycle(); 698 monoBitmap = hwMonoBitmap; 699 } 700 try (BaseIconFactory factory = getIconFactory()) { 701 entry.bitmap.setMonoIcon(monoBitmap, factory); 702 } 703 } 704 } 705 entry.bitmap.flags = c.getInt(IconDB.INDEX_FLAGS); 706 entry.bitmap = entry.bitmap.withFlags(getUserFlagOpLocked(cacheKey.user)); 707 return entry.bitmap != null; 708 } 709 710 /** 711 * Returns a cursor for an arbitrary query to the cache db 712 */ queryCacheDb(String[] columns, String selection, String[] selectionArgs)713 public synchronized Cursor queryCacheDb(String[] columns, String selection, 714 String[] selectionArgs) { 715 return mIconDb.query(columns, selection, selectionArgs); 716 } 717 718 /** 719 * Cache class to store the actual entries on disk 720 */ 721 public static final class IconDB extends SQLiteCacheHelper { 722 private static final int RELEASE_VERSION = 34; 723 724 public static final String TABLE_NAME = "icons"; 725 public static final String COLUMN_ROWID = "rowid"; 726 public static final String COLUMN_COMPONENT = "componentName"; 727 public static final String COLUMN_USER = "profileId"; 728 public static final String COLUMN_LAST_UPDATED = "lastUpdated"; 729 public static final String COLUMN_VERSION = "version"; 730 public static final String COLUMN_ICON = "icon"; 731 public static final String COLUMN_ICON_COLOR = "icon_color"; 732 public static final String COLUMN_MONO_ICON = "mono_icon"; 733 public static final String COLUMN_FLAGS = "flags"; 734 public static final String COLUMN_LABEL = "label"; 735 public static final String COLUMN_SYSTEM_STATE = "system_state"; 736 public static final String COLUMN_KEYWORDS = "keywords"; 737 738 public static final String[] COLUMNS_LOW_RES = new String[]{ 739 COLUMN_COMPONENT, 740 COLUMN_LABEL, 741 COLUMN_ICON_COLOR, 742 COLUMN_FLAGS}; 743 public static final String[] COLUMNS_HIGH_RES = Arrays.copyOf(COLUMNS_LOW_RES, 744 COLUMNS_LOW_RES.length + 2, String[].class); 745 746 static { 747 COLUMNS_HIGH_RES[COLUMNS_LOW_RES.length] = COLUMN_ICON; 748 COLUMNS_HIGH_RES[COLUMNS_LOW_RES.length + 1] = COLUMN_MONO_ICON; 749 } 750 751 private static final int INDEX_TITLE = Arrays.asList(COLUMNS_LOW_RES).indexOf(COLUMN_LABEL); 752 private static final int INDEX_COLOR = Arrays.asList(COLUMNS_LOW_RES) 753 .indexOf(COLUMN_ICON_COLOR); 754 private static final int INDEX_FLAGS = Arrays.asList(COLUMNS_LOW_RES).indexOf(COLUMN_FLAGS); 755 private static final int INDEX_ICON = COLUMNS_LOW_RES.length; 756 private static final int INDEX_MONO_ICON = INDEX_ICON + 1; 757 IconDB(Context context, String dbFileName, int iconPixelSize)758 public IconDB(Context context, String dbFileName, int iconPixelSize) { 759 super(context, dbFileName, (RELEASE_VERSION << 16) + iconPixelSize, TABLE_NAME); 760 } 761 762 @Override onCreateTable(SQLiteDatabase db)763 protected void onCreateTable(SQLiteDatabase db) { 764 db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" 765 + COLUMN_COMPONENT + " TEXT NOT NULL, " 766 + COLUMN_USER + " INTEGER NOT NULL, " 767 + COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " 768 + COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " 769 + COLUMN_ICON + " BLOB, " 770 + COLUMN_MONO_ICON + " BLOB, " 771 + COLUMN_ICON_COLOR + " INTEGER NOT NULL DEFAULT 0, " 772 + COLUMN_FLAGS + " INTEGER NOT NULL DEFAULT 0, " 773 + COLUMN_LABEL + " TEXT, " 774 + COLUMN_SYSTEM_STATE + " TEXT, " 775 + COLUMN_KEYWORDS + " TEXT, " 776 + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " 777 + ");"); 778 } 779 } 780 781 @NonNull newContentValues(@onNull final BitmapInfo bitmapInfo, @NonNull final String label, @NonNull final String packageName, @Nullable final String keywords)782 private ContentValues newContentValues(@NonNull final BitmapInfo bitmapInfo, 783 @NonNull final String label, @NonNull final String packageName, 784 @Nullable final String keywords) { 785 ContentValues values = new ContentValues(); 786 if (bitmapInfo.canPersist()) { 787 values.put(IconDB.COLUMN_ICON, flattenBitmap(bitmapInfo.icon)); 788 789 // Persist mono bitmap as alpha channel 790 Bitmap mono = bitmapInfo.getMono(); 791 if (mono != null && mono.getHeight() == bitmapInfo.icon.getHeight() 792 && mono.getWidth() == bitmapInfo.icon.getWidth() 793 && mono.getConfig() == Config.ALPHA_8) { 794 byte[] pixels = new byte[mono.getWidth() * mono.getHeight()]; 795 mono.copyPixelsToBuffer(ByteBuffer.wrap(pixels)); 796 values.put(IconDB.COLUMN_MONO_ICON, pixels); 797 } else { 798 values.put(IconDB.COLUMN_MONO_ICON, (byte[]) null); 799 } 800 } else { 801 values.put(IconDB.COLUMN_ICON, (byte[]) null); 802 values.put(IconDB.COLUMN_MONO_ICON, (byte[]) null); 803 } 804 values.put(IconDB.COLUMN_ICON_COLOR, bitmapInfo.color); 805 values.put(IconDB.COLUMN_FLAGS, bitmapInfo.flags); 806 807 values.put(IconDB.COLUMN_LABEL, label); 808 values.put(IconDB.COLUMN_SYSTEM_STATE, getIconSystemState(packageName)); 809 values.put(IconDB.COLUMN_KEYWORDS, keywords); 810 return values; 811 } 812 assertWorkerThread()813 private void assertWorkerThread() { 814 if (Looper.myLooper() != mBgLooper) { 815 throw new IllegalStateException("Cache accessed on wrong thread " + Looper.myLooper()); 816 } 817 } 818 819 /** Log to Log.d. Subclasses can override this method to log persistently for debugging. */ logdPersistently(String tag, String message, @Nullable Exception e)820 protected void logdPersistently(String tag, String message, @Nullable Exception e) { 821 Log.d(tag, message, e); 822 } 823 } 824