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