/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.icons.cache; import static com.android.launcher3.icons.BaseIconFactory.getFullResDefaultActivityIcon; import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON; import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Handler; import android.os.LocaleList; import android.os.Looper; import android.os.Process; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.icons.BaseIconFactory; import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.BitmapRenderer; import com.android.launcher3.icons.GraphicsUtils; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.SQLiteCacheHelper; import java.util.AbstractMap; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.function.Supplier; public abstract class BaseIconCache { private static final String TAG = "BaseIconCache"; private static final boolean DEBUG = false; private static final int INITIAL_ICON_CACHE_CAPACITY = 50; // Empty class name is used for storing package default entry. public static final String EMPTY_CLASS_NAME = "."; public static class CacheEntry extends BitmapInfo { public CharSequence title = ""; public CharSequence contentDescription = ""; } private final HashMap mDefaultIcons = new HashMap<>(); protected final Context mContext; protected final PackageManager mPackageManager; private final Map mCache; protected final Handler mWorkerHandler; protected int mIconDpi; protected IconDB mIconDb; protected LocaleList mLocaleList = LocaleList.getEmptyLocaleList(); protected String mSystemState = ""; private final String mDbFileName; private final BitmapFactory.Options mDecodeOptions; private final Looper mBgLooper; public BaseIconCache(Context context, String dbFileName, Looper bgLooper, int iconDpi, int iconPixelSize, boolean inMemoryCache) { mContext = context; mDbFileName = dbFileName; mPackageManager = context.getPackageManager(); mBgLooper = bgLooper; mWorkerHandler = new Handler(mBgLooper); if (inMemoryCache) { mCache = new HashMap<>(INITIAL_ICON_CACHE_CAPACITY); } else { // Use a dummy cache mCache = new AbstractMap() { @Override public Set> entrySet() { return Collections.emptySet(); } @Override public CacheEntry put(ComponentKey key, CacheEntry value) { return value; } }; } if (BitmapRenderer.USE_HARDWARE_BITMAP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { mDecodeOptions = new BitmapFactory.Options(); mDecodeOptions.inPreferredConfig = Bitmap.Config.HARDWARE; } else { mDecodeOptions = null; } updateSystemState(); mIconDpi = iconDpi; mIconDb = new IconDB(context, dbFileName, iconPixelSize); } /** * Returns the persistable serial number for {@param user}. Subclass should implement proper * caching strategy to avoid making binder call every time. */ protected abstract long getSerialNumberForUser(UserHandle user); /** * Return true if the given app is an instant app and should be badged appropriately. */ protected abstract boolean isInstantApp(ApplicationInfo info); /** * Opens and returns an icon factory. The factory is recycled by the caller. */ protected abstract BaseIconFactory getIconFactory(); public void updateIconParams(int iconDpi, int iconPixelSize) { mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize)); } private synchronized void updateIconParamsBg(int iconDpi, int iconPixelSize) { mIconDpi = iconDpi; mDefaultIcons.clear(); mIconDb.clear(); mIconDb.close(); mIconDb = new IconDB(mContext, mDbFileName, iconPixelSize); mCache.clear(); } private Drawable getFullResIcon(Resources resources, int iconId) { if (resources != null && iconId != 0) { try { return resources.getDrawableForDensity(iconId, mIconDpi); } catch (Resources.NotFoundException e) { } } return getFullResDefaultActivityIcon(mIconDpi); } public Drawable getFullResIcon(String packageName, int iconId) { try { return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId); } catch (PackageManager.NameNotFoundException e) { } return getFullResDefaultActivityIcon(mIconDpi); } public Drawable getFullResIcon(ActivityInfo info) { try { return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo), info.getIconResource()); } catch (PackageManager.NameNotFoundException e) { } return getFullResDefaultActivityIcon(mIconDpi); } private BitmapInfo makeDefaultIcon(UserHandle user) { try (BaseIconFactory li = getIconFactory()) { return li.makeDefaultIcon(user); } } /** * Remove any records for the supplied ComponentName. */ public synchronized void remove(ComponentName componentName, UserHandle user) { mCache.remove(new ComponentKey(componentName, user)); } /** * Remove any records for the supplied package name from memory. */ private void removeFromMemCacheLocked(String packageName, UserHandle user) { HashSet forDeletion = new HashSet<>(); for (ComponentKey key: mCache.keySet()) { if (key.componentName.getPackageName().equals(packageName) && key.user.equals(user)) { forDeletion.add(key); } } for (ComponentKey condemned: forDeletion) { mCache.remove(condemned); } } /** * Removes the entries related to the given package in memory and persistent DB. */ public synchronized void removeIconsForPkg(String packageName, UserHandle user) { removeFromMemCacheLocked(packageName, user); long userSerial = getSerialNumberForUser(user); mIconDb.delete( IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?", new String[]{packageName + "/%", Long.toString(userSerial)}); } public IconCacheUpdateHandler getUpdateHandler() { updateSystemState(); return new IconCacheUpdateHandler(this); } /** * Refreshes the system state definition used to check the validity of the cache. It * incorporates all the properties that can affect the cache like the list of enabled locale * and system-version. */ private void updateSystemState() { mLocaleList = mContext.getResources().getConfiguration().getLocales(); mSystemState = mLocaleList.toLanguageTags() + "," + Build.VERSION.SDK_INT; } protected String getIconSystemState(String packageName) { return mSystemState; } /** * Adds an entry into the DB and the in-memory cache. * @param replaceExisting if true, it will recreate the bitmap even if it already exists in * the memory. This is useful then the previous bitmap was created using * old data. * package private */ protected synchronized void addIconToDBAndMemCache(T object, CachingLogic cachingLogic, PackageInfo info, long userSerial, boolean replaceExisting) { UserHandle user = cachingLogic.getUser(object); ComponentName componentName = cachingLogic.getComponent(object); final ComponentKey key = new ComponentKey(componentName, user); CacheEntry entry = null; if (!replaceExisting) { entry = mCache.get(key); // We can't reuse the entry if the high-res icon is not present. if (entry == null || entry.icon == null || entry.isLowRes()) { entry = null; } } if (entry == null) { entry = new CacheEntry(); cachingLogic.loadIcon(mContext, object, entry); } // Icon can't be loaded from cachingLogic, which implies alternative icon was loaded // (e.g. fallback icon, default icon). So we drop here since there's no point in caching // an empty entry. if (entry.icon == null) return; entry.title = cachingLogic.getLabel(object); entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user); if (cachingLogic.addToMemCache()) mCache.put(key, entry); ContentValues values = newContentValues(entry, entry.title.toString(), componentName.getPackageName(), cachingLogic.getKeywords(object, mLocaleList)); addIconToDB(values, componentName, info, userSerial); } /** * Updates {@param values} to contain versioning information and adds it to the DB. * @param values {@link ContentValues} containing icon & title */ private void addIconToDB(ContentValues values, ComponentName key, PackageInfo info, long userSerial) { values.put(IconDB.COLUMN_COMPONENT, key.flattenToString()); values.put(IconDB.COLUMN_USER, userSerial); values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime); values.put(IconDB.COLUMN_VERSION, info.versionCode); mIconDb.insertOrReplace(values); } public synchronized BitmapInfo getDefaultIcon(UserHandle user) { if (!mDefaultIcons.containsKey(user)) { mDefaultIcons.put(user, makeDefaultIcon(user)); } return mDefaultIcons.get(user); } public boolean isDefaultIcon(Bitmap icon, UserHandle user) { return getDefaultIcon(user).icon == icon; } /** * Retrieves the entry from the cache. If the entry is not present, it creates a new entry. * This method is not thread safe, it must be called from a synchronized method. */ protected CacheEntry cacheLocked( @NonNull ComponentName componentName, @NonNull UserHandle user, @NonNull Supplier infoProvider, @NonNull CachingLogic cachingLogic, boolean usePackageIcon, boolean useLowResIcon) { assertWorkerThread(); ComponentKey cacheKey = new ComponentKey(componentName, user); CacheEntry entry = mCache.get(cacheKey); if (entry == null || (entry.isLowRes() && !useLowResIcon)) { entry = new CacheEntry(); if (cachingLogic.addToMemCache()) { mCache.put(cacheKey, entry); } // Check the DB first. T object = null; boolean providerFetchedOnce = false; if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) { object = infoProvider.get(); providerFetchedOnce = true; if (object != null) { cachingLogic.loadIcon(mContext, object, entry); } else { if (usePackageIcon) { CacheEntry packageEntry = getEntryForPackageLocked( componentName.getPackageName(), user, false); if (packageEntry != null) { if (DEBUG) Log.d(TAG, "using package default icon for " + componentName.toShortString()); packageEntry.applyTo(entry); entry.title = packageEntry.title; entry.contentDescription = packageEntry.contentDescription; } } if (entry.icon == null) { if (DEBUG) Log.d(TAG, "using default icon for " + componentName.toShortString()); getDefaultIcon(user).applyTo(entry); } } } if (TextUtils.isEmpty(entry.title)) { if (object == null && !providerFetchedOnce) { object = infoProvider.get(); providerFetchedOnce = true; } if (object != null) { entry.title = cachingLogic.getLabel(object); entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user); } } } return entry; } public synchronized void clear() { assertWorkerThread(); mIconDb.clear(); } /** * Adds a default package entry in the cache. This entry is not persisted and will be removed * when the cache is flushed. */ public synchronized void cachePackageInstallInfo(String packageName, UserHandle user, Bitmap icon, CharSequence title) { removeFromMemCacheLocked(packageName, user); ComponentKey cacheKey = getPackageKey(packageName, user); CacheEntry entry = mCache.get(cacheKey); // For icon caching, do not go through DB. Just update the in-memory entry. if (entry == null) { entry = new CacheEntry(); } if (!TextUtils.isEmpty(title)) { entry.title = title; } if (icon != null) { BaseIconFactory li = getIconFactory(); li.createIconBitmap(icon).applyTo(entry); li.close(); } if (!TextUtils.isEmpty(title) && entry.icon != null) { mCache.put(cacheKey, entry); } } private static ComponentKey getPackageKey(String packageName, UserHandle user) { ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME); return new ComponentKey(cn, user); } /** * Gets an entry for the package, which can be used as a fallback entry for various components. * This method is not thread safe, it must be called from a synchronized method. */ protected CacheEntry getEntryForPackageLocked(String packageName, UserHandle user, boolean useLowResIcon) { assertWorkerThread(); ComponentKey cacheKey = getPackageKey(packageName, user); CacheEntry entry = mCache.get(cacheKey); if (entry == null || (entry.isLowRes() && !useLowResIcon)) { entry = new CacheEntry(); boolean entryUpdated = true; // Check the DB first. if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) { try { int flags = Process.myUserHandle().equals(user) ? 0 : PackageManager.GET_UNINSTALLED_PACKAGES; PackageInfo info = mPackageManager.getPackageInfo(packageName, flags); ApplicationInfo appInfo = info.applicationInfo; if (appInfo == null) { throw new NameNotFoundException("ApplicationInfo is null"); } BaseIconFactory li = getIconFactory(); // Load the full res icon for the application, but if useLowResIcon is set, then // only keep the low resolution icon instead of the larger full-sized icon BitmapInfo iconInfo = li.createBadgedIconBitmap( appInfo.loadIcon(mPackageManager), user, appInfo.targetSdkVersion, isInstantApp(appInfo)); li.close(); entry.title = appInfo.loadLabel(mPackageManager); entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user); entry.icon = useLowResIcon ? LOW_RES_ICON : iconInfo.icon; entry.color = iconInfo.color; // Add the icon in the DB here, since these do not get written during // package updates. ContentValues values = newContentValues( iconInfo, entry.title.toString(), packageName, null); addIconToDB(values, cacheKey.componentName, info, getSerialNumberForUser(user)); } catch (NameNotFoundException e) { if (DEBUG) Log.d(TAG, "Application not installed " + packageName); entryUpdated = false; } } // Only add a filled-out entry to the cache if (entryUpdated) { mCache.put(cacheKey, entry); } } return entry; } private boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) { Cursor c = null; try { c = mIconDb.query( lowRes ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES, IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", new String[]{ cacheKey.componentName.flattenToString(), Long.toString(getSerialNumberForUser(cacheKey.user))}); if (c.moveToNext()) { // Set the alpha to be 255, so that we never have a wrong color entry.color = setColorAlphaBound(c.getInt(0), 255); entry.title = c.getString(1); if (entry.title == null) { entry.title = ""; entry.contentDescription = ""; } else { entry.contentDescription = mPackageManager.getUserBadgedLabel( entry.title, cacheKey.user); } if (lowRes) { entry.icon = LOW_RES_ICON; } else { byte[] data = c.getBlob(2); try { entry.icon = BitmapFactory.decodeByteArray(data, 0, data.length, mDecodeOptions); } catch (Exception e) { } } return true; } } catch (SQLiteException e) { Log.d(TAG, "Error reading icon cache", e); } finally { if (c != null) { c.close(); } } return false; } /** * Returns a cursor for an arbitrary query to the cache db */ public synchronized Cursor queryCacheDb(String[] columns, String selection, String[] selectionArgs) { return mIconDb.query(columns, selection, selectionArgs); } /** * Cache class to store the actual entries on disk */ public static final class IconDB extends SQLiteCacheHelper { private static final int RELEASE_VERSION = 27; public static final String TABLE_NAME = "icons"; public static final String COLUMN_ROWID = "rowid"; public static final String COLUMN_COMPONENT = "componentName"; public static final String COLUMN_USER = "profileId"; public static final String COLUMN_LAST_UPDATED = "lastUpdated"; public static final String COLUMN_VERSION = "version"; public static final String COLUMN_ICON = "icon"; public static final String COLUMN_ICON_COLOR = "icon_color"; public static final String COLUMN_LABEL = "label"; public static final String COLUMN_SYSTEM_STATE = "system_state"; public static final String COLUMN_KEYWORDS = "keywords"; public static final String[] COLUMNS_HIGH_RES = new String[] { IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL, IconDB.COLUMN_ICON }; public static final String[] COLUMNS_LOW_RES = new String[] { IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL }; public IconDB(Context context, String dbFileName, int iconPixelSize) { super(context, dbFileName, (RELEASE_VERSION << 16) + iconPixelSize, TABLE_NAME); } @Override protected void onCreateTable(SQLiteDatabase db) { db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + COLUMN_COMPONENT + " TEXT NOT NULL, " + COLUMN_USER + " INTEGER NOT NULL, " + COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " + COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " + COLUMN_ICON + " BLOB, " + COLUMN_ICON_COLOR + " INTEGER NOT NULL DEFAULT 0, " + COLUMN_LABEL + " TEXT, " + COLUMN_SYSTEM_STATE + " TEXT, " + COLUMN_KEYWORDS + " TEXT, " + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " + ");"); } } private ContentValues newContentValues(BitmapInfo bitmapInfo, String label, String packageName, @Nullable String keywords) { ContentValues values = new ContentValues(); values.put(IconDB.COLUMN_ICON, bitmapInfo.isLowRes() ? null : GraphicsUtils.flattenBitmap(bitmapInfo.icon)); values.put(IconDB.COLUMN_ICON_COLOR, bitmapInfo.color); values.put(IconDB.COLUMN_LABEL, label); values.put(IconDB.COLUMN_SYSTEM_STATE, getIconSystemState(packageName)); values.put(IconDB.COLUMN_KEYWORDS, keywords); return values; } private void assertWorkerThread() { if (Looper.myLooper() != mBgLooper) { throw new IllegalStateException("Cache accessed on wrong thread " + Looper.myLooper()); } } }