summaryrefslogtreecommitdiffstats
path: root/iconloaderlib
diff options
context:
space:
mode:
authorSunny Goyal <sunnygoyal@google.com>2018-11-08 10:51:05 -0800
committerSunny Goyal <sunnygoyal@google.com>2018-11-08 15:18:25 -0800
commit1a9cbd3c882e0135a9917b7438ac552b1120f720 (patch)
treee95b62c2f66b67a35881bd7005628d2084891047 /iconloaderlib
parent024659c1b070b3d55f6ac4db27aeafe2ff98bad9 (diff)
downloadandroid_packages_apps_Trebuchet-1a9cbd3c882e0135a9917b7438ac552b1120f720.tar.gz
android_packages_apps_Trebuchet-1a9cbd3c882e0135a9917b7438ac552b1120f720.tar.bz2
android_packages_apps_Trebuchet-1a9cbd3c882e0135a9917b7438ac552b1120f720.zip
Moving BaseIconCache to icon lib
Change-Id: I4fb56dcd6231a848d152e690edaf8885efbc995a
Diffstat (limited to 'iconloaderlib')
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java31
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java557
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java33
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java67
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java303
5 files changed, 989 insertions, 2 deletions
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
index 243903cc8..065a14d5f 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
@@ -26,10 +26,11 @@ import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR;
* This class will be moved to androidx library. There shouldn't be any dependency outside
* this package.
*/
-public class BaseIconFactory {
+public class BaseIconFactory implements AutoCloseable {
private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
- public static final boolean ATLEAST_OREO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+ static final boolean ATLEAST_OREO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+ static final boolean ATLEAST_P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
private final Rect mOldBounds = new Rect();
private final Context mContext;
@@ -115,6 +116,29 @@ public class BaseIconFactory {
return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, isInstantApp, null);
}
+ public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
+ int iconAppTargetSdk) {
+ return createBadgedIconBitmap(icon, user, iconAppTargetSdk, false);
+ }
+
+ public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
+ int iconAppTargetSdk, boolean isInstantApp) {
+ return createBadgedIconBitmap(icon, user, iconAppTargetSdk, isInstantApp, null);
+ }
+
+ public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
+ int iconAppTargetSdk, boolean isInstantApp, float[] scale) {
+ boolean shrinkNonAdaptiveIcons = ATLEAST_P ||
+ (ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O);
+ return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, isInstantApp, scale);
+ }
+
+ public Bitmap createScaledBitmapWithoutShadow(Drawable icon, int iconAppTargetSdk) {
+ boolean shrinkNonAdaptiveIcons = ATLEAST_P ||
+ (ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O);
+ return createScaledBitmapWithoutShadow(icon, shrinkNonAdaptiveIcons);
+ }
+
/**
* Creates bitmap using the source drawable and various parameters.
* The bitmap is visually normalized with other icons and has enough spacing to add shadow.
@@ -277,6 +301,9 @@ public class BaseIconFactory {
return bitmap;
}
+ @Override
+ public void close() { }
+
/**
* An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
* This allows the badging to be done based on the action bitmap size rather than
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
new file mode 100644
index 000000000..2fa4b6883
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
@@ -0,0 +1,557 @@
+/*
+ * 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.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.Build.VERSION;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Process;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+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.Provider;
+import com.android.launcher3.util.SQLiteCacheHelper;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+
+import androidx.annotation.NonNull;
+
+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<UserHandle, BitmapInfo> mDefaultIcons = new HashMap<>();
+
+ protected final Context mContext;
+ protected final PackageManager mPackageManager;
+
+ private final HashMap<ComponentKey, CacheEntry> mCache =
+ new HashMap<>(INITIAL_ICON_CACHE_CAPACITY);
+ protected final Handler mWorkerHandler;
+
+ protected int mIconDpi;
+ protected IconDB mIconDb;
+ 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) {
+ mContext = context;
+ mDbFileName = dbFileName;
+ mPackageManager = context.getPackageManager();
+ mBgLooper = bgLooper;
+ mWorkerHandler = new Handler(mBgLooper);
+
+ 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.close();
+ mIconDb = new IconDB(mContext, mDbFileName, iconPixelSize);
+ mCache.clear();
+ }
+
+ private Drawable getFullResDefaultActivityIcon() {
+ return Resources.getSystem().getDrawableForDensity(
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+ ? android.R.drawable.sym_def_app_icon : android.R.mipmap.sym_def_app_icon,
+ mIconDpi);
+ }
+
+ private Drawable getFullResIcon(Resources resources, int iconId) {
+ if (resources != null && iconId != 0) {
+ try {
+ return resources.getDrawableForDensity(iconId, mIconDpi);
+ } catch (Resources.NotFoundException e) { }
+ }
+ return getFullResDefaultActivityIcon();
+ }
+
+ public Drawable getFullResIcon(String packageName, int iconId) {
+ try {
+ return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId);
+ } catch (PackageManager.NameNotFoundException e) { }
+ return getFullResDefaultActivityIcon();
+ }
+
+ public Drawable getFullResIcon(ActivityInfo info) {
+ try {
+ return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo),
+ info.getIconResource());
+ } catch (PackageManager.NameNotFoundException e) { }
+ return getFullResDefaultActivityIcon();
+ }
+
+ protected BitmapInfo makeDefaultIcon(UserHandle user) {
+ try (BaseIconFactory li = getIconFactory()) {
+ return li.createBadgedIconBitmap(
+ getFullResDefaultActivityIcon(), user, VERSION.SDK_INT);
+ }
+ }
+
+ /**
+ * 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<ComponentKey> 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 locale and system-version.
+ */
+ private void updateSystemState() {
+ final String locale;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ locale = mContext.getResources().getConfiguration().getLocales().toLanguageTags();
+ } else {
+ locale = Locale.getDefault().toString();
+ }
+
+ mSystemState = locale + "," + 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 <T> void addIconToDBAndMemCache(T object, CachingLogic<T> 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);
+ }
+ entry.title = cachingLogic.getLabel(object);
+ entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
+ mCache.put(key, entry);
+
+ ContentValues values = newContentValues(entry, entry.title.toString(),
+ componentName.getPackageName());
+ 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 <T> CacheEntry cacheLocked(
+ @NonNull ComponentName componentName, @NonNull UserHandle user,
+ @NonNull Provider<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
+ boolean usePackageIcon, boolean useLowResIcon) {
+ return cacheLocked(componentName, user, infoProvider, cachingLogic, usePackageIcon,
+ useLowResIcon, true);
+ }
+
+ protected <T> CacheEntry cacheLocked(
+ @NonNull ComponentName componentName, @NonNull UserHandle user,
+ @NonNull Provider<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
+ boolean usePackageIcon, boolean useLowResIcon, boolean addToMemCache) {
+ assertWorkerThread();
+ ComponentKey cacheKey = new ComponentKey(componentName, user);
+ CacheEntry entry = mCache.get(cacheKey);
+ if (entry == null || (entry.isLowRes() && !useLowResIcon)) {
+ entry = new CacheEntry();
+ if (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);
+ 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;
+ }
+
+ static final class IconDB extends SQLiteCacheHelper {
+ private final static int RELEASE_VERSION = 25;
+
+ public final static String TABLE_NAME = "icons";
+ public final static String COLUMN_ROWID = "rowid";
+ public final static String COLUMN_COMPONENT = "componentName";
+ public final static String COLUMN_USER = "profileId";
+ public final static String COLUMN_LAST_UPDATED = "lastUpdated";
+ public final static String COLUMN_VERSION = "version";
+ public final static String COLUMN_ICON = "icon";
+ public final static String COLUMN_ICON_COLOR = "icon_color";
+ public final static String COLUMN_LABEL = "label";
+ public final static String COLUMN_SYSTEM_STATE = "system_state";
+
+ public final static String[] COLUMNS_HIGH_RES = new String[] {
+ IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL, IconDB.COLUMN_ICON };
+ public final static 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, " +
+ "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " +
+ ");");
+ }
+ }
+
+ private ContentValues newContentValues(BitmapInfo bitmapInfo, String label, String packageName) {
+ 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));
+
+ return values;
+ }
+
+ private void assertWorkerThread() {
+ if (Looper.myLooper() != mBgLooper) {
+ throw new IllegalStateException("Cache accessed on wrong thread " + Looper.myLooper());
+ }
+ }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
new file mode 100644
index 000000000..addb51fa7
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
@@ -0,0 +1,33 @@
+/*
+ * 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 android.content.ComponentName;
+import android.content.Context;
+import android.os.UserHandle;
+
+import com.android.launcher3.icons.BitmapInfo;
+
+public interface CachingLogic<T> {
+
+ ComponentName getComponent(T object);
+
+ UserHandle getUser(T object);
+
+ CharSequence getLabel(T object);
+
+ void loadIcon(Context context, T object, BitmapInfo target);
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java b/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java
new file mode 100644
index 000000000..ee5293454
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java
@@ -0,0 +1,67 @@
+/*
+ * 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 android.os.Handler;
+
+/**
+ * A runnable that can be posted to a {@link Handler} which can be canceled.
+ */
+public abstract class HandlerRunnable implements Runnable {
+
+ private final Handler mHandler;
+ private final Runnable mEndRunnable;
+
+ private boolean mEnded = false;
+ private boolean mCanceled = false;
+
+ public HandlerRunnable(Handler handler, Runnable endRunnable) {
+ mHandler = handler;
+ mEndRunnable = endRunnable;
+ }
+
+ /**
+ * Cancels this runnable from being run, only if it has not already run.
+ */
+ public void cancel() {
+ mHandler.removeCallbacks(this);
+ // TODO: This can actually cause onEnd to be called twice if the handler is already running
+ // this runnable
+ // NOTE: This is currently run on whichever thread the caller is run on.
+ mCanceled = true;
+ onEnd();
+ }
+
+ /**
+ * @return whether this runnable was canceled.
+ */
+ protected boolean isCanceled() {
+ return mCanceled;
+ }
+
+ /**
+ * To be called by the implemention of this runnable. The end callback is done on whichever
+ * thread the caller is calling from.
+ */
+ public void onEnd() {
+ if (!mEnded) {
+ mEnded = true;
+ if (mEndRunnable != null) {
+ mEndRunnable.run();
+ }
+ }
+ }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
new file mode 100644
index 000000000..3c71bd027
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
@@ -0,0 +1,303 @@
+/*
+ * 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 android.content.ComponentName;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+
+import com.android.launcher3.icons.cache.BaseIconCache.IconDB;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.Stack;
+
+/**
+ * Utility class to handle updating the Icon cache
+ */
+public class IconCacheUpdateHandler {
+
+ private static final String TAG = "IconCacheUpdateHandler";
+
+ /**
+ * In this mode, all invalid icons are marked as to-be-deleted in {@link #mItemsToDelete}.
+ * This mode is used for the first run.
+ */
+ private static final boolean MODE_SET_INVALID_ITEMS = true;
+
+ /**
+ * In this mode, any valid icon is removed from {@link #mItemsToDelete}. This is used for all
+ * subsequent runs, which essentially acts as set-union of all valid items.
+ */
+ private static final boolean MODE_CLEAR_VALID_ITEMS = false;
+
+ private static final Object ICON_UPDATE_TOKEN = new Object();
+
+ private final HashMap<String, PackageInfo> mPkgInfoMap;
+ private final BaseIconCache mIconCache;
+
+ private final HashMap<UserHandle, Set<String>> mPackagesToIgnore = new HashMap<>();
+
+ private final SparseBooleanArray mItemsToDelete = new SparseBooleanArray();
+ private boolean mFilterMode = MODE_SET_INVALID_ITEMS;
+
+ IconCacheUpdateHandler(BaseIconCache cache) {
+ mIconCache = cache;
+
+ mPkgInfoMap = new HashMap<>();
+
+ // Remove all active icon update tasks.
+ mIconCache.mWorkerHandler.removeCallbacksAndMessages(ICON_UPDATE_TOKEN);
+
+ createPackageInfoMap();
+ }
+
+ public void setPackagesToIgnore(UserHandle userHandle, Set<String> packages) {
+ mPackagesToIgnore.put(userHandle, packages);
+ }
+
+ private void createPackageInfoMap() {
+ PackageManager pm = mIconCache.mPackageManager;
+ for (PackageInfo info :
+ pm.getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES)) {
+ mPkgInfoMap.put(info.packageName, info);
+ }
+ }
+
+ /**
+ * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
+ * the DB and are updated.
+ * @return The set of packages for which icons have updated.
+ */
+ public <T> void updateIcons(List<T> apps, CachingLogic<T> cachingLogic,
+ OnUpdateCallback onUpdateCallback) {
+ // Filter the list per user
+ HashMap<UserHandle, HashMap<ComponentName, T>> userComponentMap = new HashMap<>();
+ int count = apps.size();
+ for (int i = 0; i < count; i++) {
+ T app = apps.get(i);
+ UserHandle userHandle = cachingLogic.getUser(app);
+ HashMap<ComponentName, T> componentMap = userComponentMap.get(userHandle);
+ if (componentMap == null) {
+ componentMap = new HashMap<>();
+ userComponentMap.put(userHandle, componentMap);
+ }
+ componentMap.put(cachingLogic.getComponent(app), app);
+ }
+
+ for (Entry<UserHandle, HashMap<ComponentName, T>> entry : userComponentMap.entrySet()) {
+ updateIconsPerUser(entry.getKey(), entry.getValue(), cachingLogic, onUpdateCallback);
+ }
+
+ // From now on, clear every valid item from the global valid map.
+ mFilterMode = MODE_CLEAR_VALID_ITEMS;
+ }
+
+ /**
+ * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
+ * the DB and are updated.
+ * @return The set of packages for which icons have updated.
+ */
+ @SuppressWarnings("unchecked")
+ private <T> void updateIconsPerUser(UserHandle user, HashMap<ComponentName, T> componentMap,
+ CachingLogic<T> cachingLogic, OnUpdateCallback onUpdateCallback) {
+ Set<String> ignorePackages = mPackagesToIgnore.get(user);
+ if (ignorePackages == null) {
+ ignorePackages = Collections.emptySet();
+ }
+ long userSerial = mIconCache.getSerialNumberForUser(user);
+
+ Stack<T> appsToUpdate = new Stack<>();
+
+ try (Cursor c = mIconCache.mIconDb.query(
+ new String[]{IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT,
+ IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION,
+ IconDB.COLUMN_SYSTEM_STATE},
+ IconDB.COLUMN_USER + " = ? ",
+ new String[]{Long.toString(userSerial)})) {
+
+ final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT);
+ final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED);
+ final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION);
+ final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID);
+ final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE);
+
+ while (c.moveToNext()) {
+ String cn = c.getString(indexComponent);
+ ComponentName component = ComponentName.unflattenFromString(cn);
+ PackageInfo info = mPkgInfoMap.get(component.getPackageName());
+
+ int rowId = c.getInt(rowIndex);
+ if (info == null) {
+ if (!ignorePackages.contains(component.getPackageName())) {
+
+ if (mFilterMode == MODE_SET_INVALID_ITEMS) {
+ mIconCache.remove(component, user);
+ mItemsToDelete.put(rowId, true);
+ }
+ }
+ continue;
+ }
+ if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) {
+ // Application is not present
+ continue;
+ }
+
+ long updateTime = c.getLong(indexLastUpdate);
+ int version = c.getInt(indexVersion);
+ T app = componentMap.remove(component);
+ if (version == info.versionCode && updateTime == info.lastUpdateTime &&
+ TextUtils.equals(c.getString(systemStateIndex),
+ mIconCache.getIconSystemState(info.packageName))) {
+
+ if (mFilterMode == MODE_CLEAR_VALID_ITEMS) {
+ mItemsToDelete.put(rowId, false);
+ }
+ continue;
+ }
+ if (app == null) {
+ if (mFilterMode == MODE_SET_INVALID_ITEMS) {
+ mIconCache.remove(component, user);
+ mItemsToDelete.put(rowId, true);
+ }
+ } else {
+ appsToUpdate.add(app);
+ }
+ }
+ } catch (SQLiteException e) {
+ Log.d(TAG, "Error reading icon cache", e);
+ // Continue updating whatever we have read so far
+ }
+
+ // Insert remaining apps.
+ if (!componentMap.isEmpty() || !appsToUpdate.isEmpty()) {
+ Stack<T> appsToAdd = new Stack<>();
+ appsToAdd.addAll(componentMap.values());
+ new SerializedIconUpdateTask(userSerial, user, appsToAdd, appsToUpdate, cachingLogic,
+ onUpdateCallback).scheduleNext();
+ }
+ }
+
+ /**
+ * Commits all updates as part of the update handler to disk. Not more calls should be made
+ * to this class after this.
+ */
+ public void finish() {
+ // Commit all deletes
+ int deleteCount = 0;
+ StringBuilder queryBuilder = new StringBuilder()
+ .append(IconDB.COLUMN_ROWID)
+ .append(" IN (");
+
+ int count = mItemsToDelete.size();
+ for (int i = 0; i < count; i++) {
+ if (mItemsToDelete.valueAt(i)) {
+ if (deleteCount > 0) {
+ queryBuilder.append(", ");
+ }
+ queryBuilder.append(mItemsToDelete.keyAt(i));
+ deleteCount++;
+ }
+ }
+ queryBuilder.append(')');
+
+ if (deleteCount > 0) {
+ mIconCache.mIconDb.delete(queryBuilder.toString(), null);
+ }
+ }
+
+
+ /**
+ * A runnable that updates invalid icons and adds missing icons in the DB for the provided
+ * LauncherActivityInfo list. Items are updated/added one at a time, so that the
+ * worker thread doesn't get blocked.
+ */
+ private class SerializedIconUpdateTask<T> implements Runnable {
+ private final long mUserSerial;
+ private final UserHandle mUserHandle;
+ private final Stack<T> mAppsToAdd;
+ private final Stack<T> mAppsToUpdate;
+ private final CachingLogic<T> mCachingLogic;
+ private final HashSet<String> mUpdatedPackages = new HashSet<>();
+ private final OnUpdateCallback mOnUpdateCallback;
+
+ SerializedIconUpdateTask(long userSerial, UserHandle userHandle,
+ Stack<T> appsToAdd, Stack<T> appsToUpdate, CachingLogic<T> cachingLogic,
+ OnUpdateCallback onUpdateCallback) {
+ mUserHandle = userHandle;
+ mUserSerial = userSerial;
+ mAppsToAdd = appsToAdd;
+ mAppsToUpdate = appsToUpdate;
+ mCachingLogic = cachingLogic;
+ mOnUpdateCallback = onUpdateCallback;
+ }
+
+ @Override
+ public void run() {
+ if (!mAppsToUpdate.isEmpty()) {
+ T app = mAppsToUpdate.pop();
+ String pkg = mCachingLogic.getComponent(app).getPackageName();
+ PackageInfo info = mPkgInfoMap.get(pkg);
+ mIconCache.addIconToDBAndMemCache(
+ app, mCachingLogic, info, mUserSerial, true /*replace existing*/);
+ mUpdatedPackages.add(pkg);
+
+ if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) {
+ // No more app to update. Notify callback.
+ mOnUpdateCallback.onPackageIconsUpdated(mUpdatedPackages, mUserHandle);
+ }
+
+ // Let it run one more time.
+ scheduleNext();
+ } else if (!mAppsToAdd.isEmpty()) {
+ T app = mAppsToAdd.pop();
+ PackageInfo info = mPkgInfoMap.get(mCachingLogic.getComponent(app).getPackageName());
+ // We do not check the mPkgInfoMap when generating the mAppsToAdd. Although every
+ // app should have package info, this is not guaranteed by the api
+ if (info != null) {
+ mIconCache.addIconToDBAndMemCache(app, mCachingLogic, info,
+ mUserSerial, false /*replace existing*/);
+ }
+
+ if (!mAppsToAdd.isEmpty()) {
+ scheduleNext();
+ }
+ }
+ }
+
+ public void scheduleNext() {
+ mIconCache.mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN,
+ SystemClock.uptimeMillis() + 1);
+ }
+ }
+
+ public interface OnUpdateCallback {
+
+ void onPackageIconsUpdated(HashSet<String> updatedPackages, UserHandle user);
+ }
+}