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