diff options
author | Sunny Goyal <sunnygoyal@google.com> | 2018-11-08 10:51:05 -0800 |
---|---|---|
committer | Sunny Goyal <sunnygoyal@google.com> | 2018-11-08 15:18:25 -0800 |
commit | 1a9cbd3c882e0135a9917b7438ac552b1120f720 (patch) | |
tree | e95b62c2f66b67a35881bd7005628d2084891047 /iconloaderlib/src | |
parent | 024659c1b070b3d55f6ac4db27aeafe2ff98bad9 (diff) | |
download | packages_apps_Trebuchet-1a9cbd3c882e0135a9917b7438ac552b1120f720.tar.gz packages_apps_Trebuchet-1a9cbd3c882e0135a9917b7438ac552b1120f720.tar.bz2 packages_apps_Trebuchet-1a9cbd3c882e0135a9917b7438ac552b1120f720.zip |
Moving BaseIconCache to icon lib
Change-Id: I4fb56dcd6231a848d152e690edaf8885efbc995a
Diffstat (limited to 'iconloaderlib/src')
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); + } +} |