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 com.android.launcher3.icons.BaseIconFactory.getFullResDefaultActivityIcon; 19 import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON; 20 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; 21 22 import android.content.ComponentName; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.content.pm.ActivityInfo; 26 import android.content.pm.ApplicationInfo; 27 import android.content.pm.PackageInfo; 28 import android.content.pm.PackageManager; 29 import android.content.pm.PackageManager.NameNotFoundException; 30 import android.content.res.Resources; 31 import android.database.Cursor; 32 import android.database.sqlite.SQLiteDatabase; 33 import android.database.sqlite.SQLiteException; 34 import android.graphics.Bitmap; 35 import android.graphics.drawable.Drawable; 36 import android.os.Build; 37 import android.os.Handler; 38 import android.os.LocaleList; 39 import android.os.Looper; 40 import android.os.Process; 41 import android.os.UserHandle; 42 import android.text.TextUtils; 43 import android.util.Log; 44 45 import androidx.annotation.NonNull; 46 import androidx.annotation.Nullable; 47 import androidx.annotation.VisibleForTesting; 48 49 import com.android.launcher3.icons.BaseIconFactory; 50 import com.android.launcher3.icons.BitmapInfo; 51 import com.android.launcher3.util.ComponentKey; 52 import com.android.launcher3.util.SQLiteCacheHelper; 53 54 import java.util.AbstractMap; 55 import java.util.Collections; 56 import java.util.HashMap; 57 import java.util.HashSet; 58 import java.util.Map; 59 import java.util.Set; 60 import java.util.function.Supplier; 61 62 public abstract class BaseIconCache { 63 64 private static final String TAG = "BaseIconCache"; 65 private static final boolean DEBUG = false; 66 67 private static final int INITIAL_ICON_CACHE_CAPACITY = 50; 68 69 // Empty class name is used for storing package default entry. 70 public static final String EMPTY_CLASS_NAME = "."; 71 72 public static class CacheEntry { 73 74 @NonNull 75 public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO; 76 public CharSequence title = ""; 77 public CharSequence contentDescription = ""; 78 } 79 80 private final HashMap<UserHandle, BitmapInfo> mDefaultIcons = new HashMap<>(); 81 82 protected final Context mContext; 83 protected final PackageManager mPackageManager; 84 85 private final Map<ComponentKey, CacheEntry> mCache; 86 protected final Handler mWorkerHandler; 87 88 protected int mIconDpi; 89 protected IconDB mIconDb; 90 protected LocaleList mLocaleList = LocaleList.getEmptyLocaleList(); 91 protected String mSystemState = ""; 92 93 private final String mDbFileName; 94 private final Looper mBgLooper; 95 BaseIconCache(Context context, String dbFileName, Looper bgLooper, int iconDpi, int iconPixelSize, boolean inMemoryCache)96 public BaseIconCache(Context context, String dbFileName, Looper bgLooper, 97 int iconDpi, int iconPixelSize, boolean inMemoryCache) { 98 mContext = context; 99 mDbFileName = dbFileName; 100 mPackageManager = context.getPackageManager(); 101 mBgLooper = bgLooper; 102 mWorkerHandler = new Handler(mBgLooper); 103 104 if (inMemoryCache) { 105 mCache = new HashMap<>(INITIAL_ICON_CACHE_CAPACITY); 106 } else { 107 // Use a dummy cache 108 mCache = new AbstractMap<ComponentKey, CacheEntry>() { 109 @Override 110 public Set<Entry<ComponentKey, CacheEntry>> entrySet() { 111 return Collections.emptySet(); 112 } 113 114 @Override 115 public CacheEntry put(ComponentKey key, CacheEntry value) { 116 return value; 117 } 118 }; 119 } 120 121 updateSystemState(); 122 mIconDpi = iconDpi; 123 mIconDb = new IconDB(context, dbFileName, iconPixelSize); 124 } 125 126 /** 127 * Returns the persistable serial number for {@param user}. Subclass should implement proper 128 * caching strategy to avoid making binder call every time. 129 */ getSerialNumberForUser(UserHandle user)130 protected abstract long getSerialNumberForUser(UserHandle user); 131 132 /** 133 * Return true if the given app is an instant app and should be badged appropriately. 134 */ isInstantApp(ApplicationInfo info)135 protected abstract boolean isInstantApp(ApplicationInfo info); 136 137 /** 138 * Opens and returns an icon factory. The factory is recycled by the caller. 139 */ getIconFactory()140 public abstract BaseIconFactory getIconFactory(); 141 updateIconParams(int iconDpi, int iconPixelSize)142 public void updateIconParams(int iconDpi, int iconPixelSize) { 143 mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize)); 144 } 145 updateIconParamsBg(int iconDpi, int iconPixelSize)146 private synchronized void updateIconParamsBg(int iconDpi, int iconPixelSize) { 147 mIconDpi = iconDpi; 148 mDefaultIcons.clear(); 149 mIconDb.clear(); 150 mIconDb.close(); 151 mIconDb = new IconDB(mContext, mDbFileName, iconPixelSize); 152 mCache.clear(); 153 } 154 getFullResIcon(Resources resources, int iconId)155 private Drawable getFullResIcon(Resources resources, int iconId) { 156 if (resources != null && iconId != 0) { 157 try { 158 return resources.getDrawableForDensity(iconId, mIconDpi); 159 } catch (Resources.NotFoundException e) { } 160 } 161 return getFullResDefaultActivityIcon(mIconDpi); 162 } 163 getFullResIcon(String packageName, int iconId)164 public Drawable getFullResIcon(String packageName, int iconId) { 165 try { 166 return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId); 167 } catch (PackageManager.NameNotFoundException e) { } 168 return getFullResDefaultActivityIcon(mIconDpi); 169 } 170 getFullResIcon(ActivityInfo info)171 public Drawable getFullResIcon(ActivityInfo info) { 172 try { 173 return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo), 174 info.getIconResource()); 175 } catch (PackageManager.NameNotFoundException e) { } 176 return getFullResDefaultActivityIcon(mIconDpi); 177 } 178 makeDefaultIcon(UserHandle user)179 private BitmapInfo makeDefaultIcon(UserHandle user) { 180 try (BaseIconFactory li = getIconFactory()) { 181 return li.makeDefaultIcon(user); 182 } 183 } 184 185 /** 186 * Remove any records for the supplied ComponentName. 187 */ remove(ComponentName componentName, UserHandle user)188 public synchronized void remove(ComponentName componentName, UserHandle user) { 189 mCache.remove(new ComponentKey(componentName, user)); 190 } 191 192 /** 193 * Remove any records for the supplied package name from memory. 194 */ removeFromMemCacheLocked(String packageName, UserHandle user)195 private void removeFromMemCacheLocked(String packageName, UserHandle user) { 196 HashSet<ComponentKey> forDeletion = new HashSet<>(); 197 for (ComponentKey key: mCache.keySet()) { 198 if (key.componentName.getPackageName().equals(packageName) 199 && key.user.equals(user)) { 200 forDeletion.add(key); 201 } 202 } 203 for (ComponentKey condemned: forDeletion) { 204 mCache.remove(condemned); 205 } 206 } 207 208 /** 209 * Removes the entries related to the given package in memory and persistent DB. 210 */ removeIconsForPkg(String packageName, UserHandle user)211 public synchronized void removeIconsForPkg(String packageName, UserHandle user) { 212 removeFromMemCacheLocked(packageName, user); 213 long userSerial = getSerialNumberForUser(user); 214 mIconDb.delete( 215 IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?", 216 new String[]{packageName + "/%", Long.toString(userSerial)}); 217 } 218 getUpdateHandler()219 public IconCacheUpdateHandler getUpdateHandler() { 220 updateSystemState(); 221 return new IconCacheUpdateHandler(this); 222 } 223 224 /** 225 * Refreshes the system state definition used to check the validity of the cache. It 226 * incorporates all the properties that can affect the cache like the list of enabled locale 227 * and system-version. 228 */ updateSystemState()229 private void updateSystemState() { 230 mLocaleList = mContext.getResources().getConfiguration().getLocales(); 231 mSystemState = mLocaleList.toLanguageTags() + "," + Build.VERSION.SDK_INT; 232 } 233 getIconSystemState(String packageName)234 protected String getIconSystemState(String packageName) { 235 return mSystemState; 236 } 237 238 /** 239 * Adds an entry into the DB and the in-memory cache. 240 * @param replaceExisting if true, it will recreate the bitmap even if it already exists in 241 * the memory. This is useful then the previous bitmap was created using 242 * old data. 243 */ 244 @VisibleForTesting addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic, PackageInfo info, long userSerial, boolean replaceExisting)245 public synchronized <T> void addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic, 246 PackageInfo info, long userSerial, boolean replaceExisting) { 247 UserHandle user = cachingLogic.getUser(object); 248 ComponentName componentName = cachingLogic.getComponent(object); 249 250 final ComponentKey key = new ComponentKey(componentName, user); 251 CacheEntry entry = null; 252 if (!replaceExisting) { 253 entry = mCache.get(key); 254 // We can't reuse the entry if the high-res icon is not present. 255 if (entry == null || entry.bitmap.isNullOrLowRes()) { 256 entry = null; 257 } 258 } 259 if (entry == null) { 260 entry = new CacheEntry(); 261 entry.bitmap = cachingLogic.loadIcon(mContext, object); 262 } 263 // Icon can't be loaded from cachingLogic, which implies alternative icon was loaded 264 // (e.g. fallback icon, default icon). So we drop here since there's no point in caching 265 // an empty entry. 266 if (entry.bitmap.isNullOrLowRes()) return; 267 entry.title = cachingLogic.getLabel(object); 268 entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user); 269 if (cachingLogic.addToMemCache()) mCache.put(key, entry); 270 271 ContentValues values = newContentValues(entry.bitmap, entry.title.toString(), 272 componentName.getPackageName(), cachingLogic.getKeywords(object, mLocaleList)); 273 addIconToDB(values, componentName, info, userSerial, 274 cachingLogic.getLastUpdatedTime(object, info)); 275 } 276 277 /** 278 * Updates {@param values} to contain versioning information and adds it to the DB. 279 * @param values {@link ContentValues} containing icon & title 280 */ addIconToDB(ContentValues values, ComponentName key, PackageInfo info, long userSerial, long lastUpdateTime)281 private void addIconToDB(ContentValues values, ComponentName key, 282 PackageInfo info, long userSerial, long lastUpdateTime) { 283 values.put(IconDB.COLUMN_COMPONENT, key.flattenToString()); 284 values.put(IconDB.COLUMN_USER, userSerial); 285 values.put(IconDB.COLUMN_LAST_UPDATED, lastUpdateTime); 286 values.put(IconDB.COLUMN_VERSION, info.versionCode); 287 mIconDb.insertOrReplace(values); 288 } 289 getDefaultIcon(UserHandle user)290 public synchronized BitmapInfo getDefaultIcon(UserHandle user) { 291 if (!mDefaultIcons.containsKey(user)) { 292 mDefaultIcons.put(user, makeDefaultIcon(user)); 293 } 294 return mDefaultIcons.get(user); 295 } 296 isDefaultIcon(BitmapInfo icon, UserHandle user)297 public boolean isDefaultIcon(BitmapInfo icon, UserHandle user) { 298 return getDefaultIcon(user).icon == icon.icon; 299 } 300 301 /** 302 * Retrieves the entry from the cache. If the entry is not present, it creates a new entry. 303 * This method is not thread safe, it must be called from a synchronized method. 304 */ cacheLocked( @onNull ComponentName componentName, @NonNull UserHandle user, @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic, boolean usePackageIcon, boolean useLowResIcon)305 protected <T> CacheEntry cacheLocked( 306 @NonNull ComponentName componentName, @NonNull UserHandle user, 307 @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic, 308 boolean usePackageIcon, boolean useLowResIcon) { 309 assertWorkerThread(); 310 ComponentKey cacheKey = new ComponentKey(componentName, user); 311 CacheEntry entry = mCache.get(cacheKey); 312 if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) { 313 entry = new CacheEntry(); 314 if (cachingLogic.addToMemCache()) { 315 mCache.put(cacheKey, entry); 316 } 317 318 // Check the DB first. 319 T object = null; 320 boolean providerFetchedOnce = false; 321 322 if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) { 323 object = infoProvider.get(); 324 providerFetchedOnce = true; 325 326 if (object != null) { 327 entry.bitmap = cachingLogic.loadIcon(mContext, object); 328 } else { 329 if (usePackageIcon) { 330 CacheEntry packageEntry = getEntryForPackageLocked( 331 componentName.getPackageName(), user, false); 332 if (packageEntry != null) { 333 if (DEBUG) Log.d(TAG, "using package default icon for " + 334 componentName.toShortString()); 335 entry.bitmap = packageEntry.bitmap; 336 entry.title = packageEntry.title; 337 entry.contentDescription = packageEntry.contentDescription; 338 } 339 } 340 if (entry.bitmap == null) { 341 if (DEBUG) Log.d(TAG, "using default icon for " + 342 componentName.toShortString()); 343 entry.bitmap = getDefaultIcon(user); 344 } 345 } 346 } 347 348 if (TextUtils.isEmpty(entry.title)) { 349 if (object == null && !providerFetchedOnce) { 350 object = infoProvider.get(); 351 providerFetchedOnce = true; 352 } 353 if (object != null) { 354 entry.title = cachingLogic.getLabel(object); 355 entry.contentDescription = mPackageManager.getUserBadgedLabel( 356 cachingLogic.getDescription(object, entry.title), user); 357 } 358 } 359 } 360 return entry; 361 } 362 clear()363 public synchronized void clear() { 364 assertWorkerThread(); 365 mIconDb.clear(); 366 } 367 368 /** 369 * Adds a default package entry in the cache. This entry is not persisted and will be removed 370 * when the cache is flushed. 371 */ cachePackageInstallInfo(String packageName, UserHandle user, Bitmap icon, CharSequence title)372 protected synchronized void cachePackageInstallInfo(String packageName, UserHandle user, 373 Bitmap icon, CharSequence title) { 374 removeFromMemCacheLocked(packageName, user); 375 376 ComponentKey cacheKey = getPackageKey(packageName, user); 377 CacheEntry entry = mCache.get(cacheKey); 378 379 // For icon caching, do not go through DB. Just update the in-memory entry. 380 if (entry == null) { 381 entry = new CacheEntry(); 382 } 383 if (!TextUtils.isEmpty(title)) { 384 entry.title = title; 385 } 386 if (icon != null) { 387 BaseIconFactory li = getIconFactory(); 388 entry.bitmap = li.createShapedIconBitmap(icon, user); 389 li.close(); 390 } 391 if (!TextUtils.isEmpty(title) && entry.bitmap.icon != null) { 392 mCache.put(cacheKey, entry); 393 } 394 } 395 getPackageKey(String packageName, UserHandle user)396 private static ComponentKey getPackageKey(String packageName, UserHandle user) { 397 ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME); 398 return new ComponentKey(cn, user); 399 } 400 401 /** 402 * Gets an entry for the package, which can be used as a fallback entry for various components. 403 * This method is not thread safe, it must be called from a synchronized method. 404 */ getEntryForPackageLocked(String packageName, UserHandle user, boolean useLowResIcon)405 protected CacheEntry getEntryForPackageLocked(String packageName, UserHandle user, 406 boolean useLowResIcon) { 407 assertWorkerThread(); 408 ComponentKey cacheKey = getPackageKey(packageName, user); 409 CacheEntry entry = mCache.get(cacheKey); 410 411 if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) { 412 entry = new CacheEntry(); 413 boolean entryUpdated = true; 414 415 // Check the DB first. 416 if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) { 417 try { 418 int flags = Process.myUserHandle().equals(user) ? 0 : 419 PackageManager.GET_UNINSTALLED_PACKAGES; 420 PackageInfo info = mPackageManager.getPackageInfo(packageName, flags); 421 ApplicationInfo appInfo = info.applicationInfo; 422 if (appInfo == null) { 423 throw new NameNotFoundException("ApplicationInfo is null"); 424 } 425 426 BaseIconFactory li = getIconFactory(); 427 // Load the full res icon for the application, but if useLowResIcon is set, then 428 // only keep the low resolution icon instead of the larger full-sized icon 429 BitmapInfo iconInfo = li.createBadgedIconBitmap( 430 appInfo.loadIcon(mPackageManager), user, appInfo.targetSdkVersion, 431 isInstantApp(appInfo)); 432 li.close(); 433 434 entry.title = appInfo.loadLabel(mPackageManager); 435 entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user); 436 entry.bitmap = BitmapInfo.of( 437 useLowResIcon ? LOW_RES_ICON : iconInfo.icon, iconInfo.color); 438 439 // Add the icon in the DB here, since these do not get written during 440 // package updates. 441 ContentValues values = newContentValues( 442 iconInfo, entry.title.toString(), packageName, null); 443 addIconToDB(values, cacheKey.componentName, info, getSerialNumberForUser(user), 444 info.lastUpdateTime); 445 446 } catch (NameNotFoundException e) { 447 if (DEBUG) Log.d(TAG, "Application not installed " + packageName); 448 entryUpdated = false; 449 } 450 } 451 452 // Only add a filled-out entry to the cache 453 if (entryUpdated) { 454 mCache.put(cacheKey, entry); 455 } 456 } 457 return entry; 458 } 459 getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes)460 protected boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) { 461 Cursor c = null; 462 try { 463 c = mIconDb.query( 464 lowRes ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES, 465 IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", 466 new String[]{ 467 cacheKey.componentName.flattenToString(), 468 Long.toString(getSerialNumberForUser(cacheKey.user))}); 469 if (c.moveToNext()) { 470 // Set the alpha to be 255, so that we never have a wrong color 471 entry.bitmap = BitmapInfo.of(LOW_RES_ICON, setColorAlphaBound(c.getInt(0), 255)); 472 entry.title = c.getString(1); 473 if (entry.title == null) { 474 entry.title = ""; 475 entry.contentDescription = ""; 476 } else { 477 entry.contentDescription = mPackageManager.getUserBadgedLabel( 478 entry.title, cacheKey.user); 479 } 480 481 if (!lowRes) { 482 try { 483 entry.bitmap = BitmapInfo.fromByteArray( 484 c.getBlob(2), entry.bitmap.color, cacheKey.user, this, mContext); 485 } catch (Exception e) { 486 return false; 487 } 488 } 489 return entry.bitmap != null; 490 } 491 } catch (SQLiteException e) { 492 Log.d(TAG, "Error reading icon cache", e); 493 } finally { 494 if (c != null) { 495 c.close(); 496 } 497 } 498 return false; 499 } 500 501 /** 502 * Returns a cursor for an arbitrary query to the cache db 503 */ queryCacheDb(String[] columns, String selection, String[] selectionArgs)504 public synchronized Cursor queryCacheDb(String[] columns, String selection, 505 String[] selectionArgs) { 506 return mIconDb.query(columns, selection, selectionArgs); 507 } 508 509 /** 510 * Cache class to store the actual entries on disk 511 */ 512 public static final class IconDB extends SQLiteCacheHelper { 513 private static final int RELEASE_VERSION = 31; 514 515 public static final String TABLE_NAME = "icons"; 516 public static final String COLUMN_ROWID = "rowid"; 517 public static final String COLUMN_COMPONENT = "componentName"; 518 public static final String COLUMN_USER = "profileId"; 519 public static final String COLUMN_LAST_UPDATED = "lastUpdated"; 520 public static final String COLUMN_VERSION = "version"; 521 public static final String COLUMN_ICON = "icon"; 522 public static final String COLUMN_ICON_COLOR = "icon_color"; 523 public static final String COLUMN_LABEL = "label"; 524 public static final String COLUMN_SYSTEM_STATE = "system_state"; 525 public static final String COLUMN_KEYWORDS = "keywords"; 526 527 public static final String[] COLUMNS_HIGH_RES = new String[] { 528 IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL, IconDB.COLUMN_ICON }; 529 public static final String[] COLUMNS_LOW_RES = new String[] { 530 IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL }; 531 IconDB(Context context, String dbFileName, int iconPixelSize)532 public IconDB(Context context, String dbFileName, int iconPixelSize) { 533 super(context, dbFileName, (RELEASE_VERSION << 16) + iconPixelSize, TABLE_NAME); 534 } 535 536 @Override onCreateTable(SQLiteDatabase db)537 protected void onCreateTable(SQLiteDatabase db) { 538 db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" 539 + COLUMN_COMPONENT + " TEXT NOT NULL, " 540 + COLUMN_USER + " INTEGER NOT NULL, " 541 + COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " 542 + COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " 543 + COLUMN_ICON + " BLOB, " 544 + COLUMN_ICON_COLOR + " INTEGER NOT NULL DEFAULT 0, " 545 + COLUMN_LABEL + " TEXT, " 546 + COLUMN_SYSTEM_STATE + " TEXT, " 547 + COLUMN_KEYWORDS + " TEXT, " 548 + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " 549 + ");"); 550 } 551 } 552 newContentValues(BitmapInfo bitmapInfo, String label, String packageName, @Nullable String keywords)553 private ContentValues newContentValues(BitmapInfo bitmapInfo, String label, 554 String packageName, @Nullable String keywords) { 555 ContentValues values = new ContentValues(); 556 values.put(IconDB.COLUMN_ICON, bitmapInfo.toByteArray()); 557 values.put(IconDB.COLUMN_ICON_COLOR, bitmapInfo.color); 558 559 values.put(IconDB.COLUMN_LABEL, label); 560 values.put(IconDB.COLUMN_SYSTEM_STATE, getIconSystemState(packageName)); 561 values.put(IconDB.COLUMN_KEYWORDS, keywords); 562 return values; 563 } 564 assertWorkerThread()565 private void assertWorkerThread() { 566 if (Looper.myLooper() != mBgLooper) { 567 throw new IllegalStateException("Cache accessed on wrong thread " + Looper.myLooper()); 568 } 569 } 570 } 571