From e612775922ec9f8cc4e5cb976bc62b3312a3de0e Mon Sep 17 00:00:00 2001 From: Hyunyoung Song Date: Tue, 21 Jul 2015 17:01:26 -0700 Subject: resolved conflicts for merge of 13ef17a3 to mnc-dr-dev b/22609402 Change-Id: I140cf972d57e14737a6f91c0b4a8ec6c7ff1af2b --- src/com/android/launcher3/IconCache.java | 798 ++++++++++++++++++++++--------- 1 file changed, 569 insertions(+), 229 deletions(-) (limited to 'src/com/android/launcher3/IconCache.java') diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java index 5a0875b30..916418f18 100644 --- a/src/com/android/launcher3/IconCache.java +++ b/src/com/android/launcher3/IconCache.java @@ -16,19 +16,28 @@ package com.android.launcher3; -import android.app.ActivityManager; import android.content.ComponentName; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; 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.SQLiteOpenHelper; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; @@ -36,17 +45,17 @@ import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.model.PackageItemInfo; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.Thunk; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; -import java.util.Map.Entry; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.Stack; /** * Cache of application icons. Icons can be made from any thread. @@ -56,66 +65,71 @@ public class IconCache { private static final String TAG = "Launcher.IconCache"; private static final int INITIAL_ICON_CACHE_CAPACITY = 50; - private static final String RESOURCE_FILE_PREFIX = "icon_"; // Empty class name is used for storing package default entry. private static final String EMPTY_CLASS_NAME = "."; private static final boolean DEBUG = false; - private static class CacheEntry { - public Bitmap icon; - public CharSequence title; - public CharSequence contentDescription; - } - - private static class CacheKey { - public ComponentName componentName; - public UserHandleCompat user; + private static final int LOW_RES_SCALE_FACTOR = 5; - CacheKey(ComponentName componentName, UserHandleCompat user) { - this.componentName = componentName; - this.user = user; - } + @Thunk static final Object ICON_UPDATE_TOKEN = new Object(); - @Override - public int hashCode() { - return componentName.hashCode() + user.hashCode(); - } - - @Override - public boolean equals(Object o) { - CacheKey other = (CacheKey) o; - return other.componentName.equals(componentName) && other.user.equals(user); - } + @Thunk static class CacheEntry { + public Bitmap icon; + public CharSequence title = ""; + public CharSequence contentDescription = ""; + public boolean isLowResIcon; } - private final HashMap mDefaultIcons = - new HashMap(); + private final HashMap mDefaultIcons = new HashMap<>(); + @Thunk final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); + private final Context mContext; private final PackageManager mPackageManager; - private final UserManagerCompat mUserManager; + @Thunk final UserManagerCompat mUserManager; private final LauncherAppsCompat mLauncherApps; - private final HashMap mCache = - new HashMap(INITIAL_ICON_CACHE_CAPACITY); - private int mIconDpi; - - public IconCache(Context context) { - ActivityManager activityManager = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - + private final HashMap mCache = + new HashMap(INITIAL_ICON_CACHE_CAPACITY); + private final int mIconDpi; + @Thunk final IconDB mIconDb; + + @Thunk final Handler mWorkerHandler; + + // The background color used for activity icons. Since these icons are displayed in all-apps + // and folders, this would be same as the light quantum panel background. This color + // is used to convert icons to RGB_565. + private final int mActivityBgColor; + // The background color used for package icons. These are displayed in widget tray, which + // has a dark quantum panel background. + private final int mPackageBgColor; + private final BitmapFactory.Options mLowResOptions; + + private String mSystemState; + private Bitmap mLowResBitmap; + private Canvas mLowResCanvas; + private Paint mLowResPaint; + + public IconCache(Context context, InvariantDeviceProfile inv) { mContext = context; mPackageManager = context.getPackageManager(); mUserManager = UserManagerCompat.getInstance(mContext); mLauncherApps = LauncherAppsCompat.getInstance(mContext); - mIconDpi = activityManager.getLauncherLargeIconDensity(); - - // need to set mIconDpi before getting default icon - UserHandleCompat myUser = UserHandleCompat.myUserHandle(); - mDefaultIcons.put(myUser, makeDefaultIcon(myUser)); + mIconDpi = inv.fillResIconDpi; + mIconDb = new IconDB(context); + + mWorkerHandler = new Handler(LauncherModel.getWorkerLooper()); + + mActivityBgColor = context.getResources().getColor(R.color.quantum_panel_bg_color); + mPackageBgColor = context.getResources().getColor(R.color.quantum_panel_bg_color_dark); + mLowResOptions = new BitmapFactory.Options(); + // Always prefer RGB_565 config for low res. If the bitmap has transparency, it will + // automatically be loaded as ALPHA_8888. + mLowResOptions.inPreferredConfig = Bitmap.Config.RGB_565; + updateSystemStateString(); } - public Drawable getFullResDefaultActivityIcon() { + private Drawable getFullResDefaultActivityIcon() { return getFullResIcon(Resources.getSystem(), android.R.mipmap.sym_def_app_icon); } @@ -145,10 +159,6 @@ public class IconCache { return getFullResDefaultActivityIcon(); } - public int getFullResIconDpi() { - return mIconDpi; - } - public Drawable getFullResIcon(ActivityInfo info) { Resources resources; try { @@ -184,59 +194,269 @@ public class IconCache { * Remove any records for the supplied ComponentName. */ public synchronized void remove(ComponentName componentName, UserHandleCompat user) { - mCache.remove(new CacheKey(componentName, user)); + mCache.remove(new ComponentKey(componentName, user)); } /** - * Remove any records for the supplied package name. + * Remove any records for the supplied package name from memory. */ - public synchronized void remove(String packageName, UserHandleCompat user) { - HashSet forDeletion = new HashSet(); - for (CacheKey key: mCache.keySet()) { + private void removeFromMemCacheLocked(String packageName, UserHandleCompat user) { + HashSet forDeletion = new HashSet(); + for (ComponentKey key: mCache.keySet()) { if (key.componentName.getPackageName().equals(packageName) && key.user.equals(user)) { forDeletion.add(key); } } - for (CacheKey condemned: forDeletion) { + for (ComponentKey condemned: forDeletion) { mCache.remove(condemned); } } /** - * Empty out the cache. + * Updates the entries related to the given package in memory and persistent DB. */ - public synchronized void flush() { - mCache.clear(); + public synchronized void updateIconsForPkg(String packageName, UserHandleCompat user) { + removeIconsForPkg(packageName, user); + try { + PackageInfo info = mPackageManager.getPackageInfo(packageName, + PackageManager.GET_UNINSTALLED_PACKAGES); + long userSerial = mUserManager.getSerialNumberForUser(user); + for (LauncherActivityInfoCompat app : mLauncherApps.getActivityList(packageName, user)) { + addIconToDBAndMemCache(app, info, userSerial); + } + } catch (NameNotFoundException e) { + Log.d(TAG, "Package not found", e); + return; + } } /** - * Empty out the cache that aren't of the correct grid size + * Removes the entries related to the given package in memory and persistent DB. */ - public synchronized void flushInvalidIcons(DeviceProfile grid) { - Iterator> it = mCache.entrySet().iterator(); - while (it.hasNext()) { - final CacheEntry e = it.next().getValue(); - if ((e.icon != null) && (e.icon.getWidth() < grid.iconSizePx - || e.icon.getHeight() < grid.iconSizePx)) { - it.remove(); + public synchronized void removeIconsForPkg(String packageName, UserHandleCompat user) { + removeFromMemCacheLocked(packageName, user); + long userSerial = mUserManager.getSerialNumberForUser(user); + mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME, + IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?", + new String[] {packageName + "/%", Long.toString(userSerial)}); + } + + public void updateDbIcons(Set ignorePackagesForMainUser) { + // Remove all active icon update tasks. + mWorkerHandler.removeCallbacksAndMessages(ICON_UPDATE_TOKEN); + + updateSystemStateString(); + for (UserHandleCompat user : mUserManager.getUserProfiles()) { + // Query for the set of apps + final List apps = mLauncherApps.getActivityList(null, user); + // Fail if we don't have any apps + // TODO: Fix this. Only fail for the current user. + if (apps == null || apps.isEmpty()) { + return; } + + // Update icon cache. This happens in segments and {@link #onPackageIconsUpdated} + // is called by the icon cache when the job is complete. + updateDBIcons(user, apps, UserHandleCompat.myUserHandle().equals(user) + ? ignorePackagesForMainUser : Collections.emptySet()); } } /** - * Fill in "application" with the icon and label for "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. + */ + private void updateDBIcons(UserHandleCompat user, List apps, + Set ignorePackages) { + long userSerial = mUserManager.getSerialNumberForUser(user); + PackageManager pm = mContext.getPackageManager(); + HashMap pkgInfoMap = new HashMap(); + for (PackageInfo info : pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) { + pkgInfoMap.put(info.packageName, info); + } + + HashMap componentMap = new HashMap<>(); + for (LauncherActivityInfoCompat app : apps) { + componentMap.put(app.getComponentName(), app); + } + + Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME, + 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)}, + null, null, null); + + 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); + + HashSet itemsToRemove = new HashSet(); + Stack appsToUpdate = new Stack<>(); + + while (c.moveToNext()) { + String cn = c.getString(indexComponent); + ComponentName component = ComponentName.unflattenFromString(cn); + PackageInfo info = pkgInfoMap.get(component.getPackageName()); + if (info == null) { + if (!ignorePackages.contains(component.getPackageName())) { + remove(component, user); + itemsToRemove.add(c.getInt(rowIndex)); + } + 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); + LauncherActivityInfoCompat app = componentMap.remove(component); + if (version == info.versionCode && updateTime == info.lastUpdateTime && + TextUtils.equals(mSystemState, c.getString(systemStateIndex))) { + continue; + } + if (app == null) { + remove(component, user); + itemsToRemove.add(c.getInt(rowIndex)); + } else { + appsToUpdate.add(app); + } + } + c.close(); + if (!itemsToRemove.isEmpty()) { + mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME, + Utilities.createDbSelectionQuery(IconDB.COLUMN_ROWID, itemsToRemove), + null); + } + + // Insert remaining apps. + if (!componentMap.isEmpty() || !appsToUpdate.isEmpty()) { + Stack appsToAdd = new Stack<>(); + appsToAdd.addAll(componentMap.values()); + new SerializedIconUpdateTask(userSerial, pkgInfoMap, + appsToAdd, appsToUpdate).scheduleNext(); + } + } + + @Thunk void addIconToDBAndMemCache(LauncherActivityInfoCompat app, PackageInfo info, + long userSerial) { + // Reuse the existing entry if it already exists in the DB. This ensures that we do not + // create bitmap if it was already created during loader. + ContentValues values = updateCacheAndGetContentValues(app, false); + addIconToDB(values, app.getComponentName(), info, userSerial); + } + + /** + * Updates {@param values} to contain versoning information and adds it to the DB. + * @param values {@link ContentValues} containing icon & title */ - public synchronized void getTitleAndIcon(AppInfo application, LauncherActivityInfoCompat info, - HashMap labelCache) { - CacheEntry entry = cacheLocked(application.componentName, info, labelCache, - info.getUser(), false); + 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.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } + + @Thunk ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app, + boolean replaceExisting) { + final ComponentKey key = new ComponentKey(app.getComponentName(), app.getUser()); + 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.isLowResIcon || entry.icon == null) { + entry = null; + } + } + if (entry == null) { + entry = new CacheEntry(); + entry.icon = Utilities.createIconBitmap(app.getBadgedIcon(mIconDpi), mContext); + } + entry.title = app.getLabel(); + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, app.getUser()); + mCache.put(new ComponentKey(app.getComponentName(), app.getUser()), entry); + + return newContentValues(entry.icon, entry.title.toString(), mActivityBgColor); + } + + /** + * Fetches high-res icon for the provided ItemInfo and updates the caller when done. + * @return a request ID that can be used to cancel the request. + */ + public IconLoadRequest updateIconInBackground(final BubbleTextView caller, final ItemInfo info) { + Runnable request = new Runnable() { + + @Override + public void run() { + if (info instanceof AppInfo) { + getTitleAndIcon((AppInfo) info, null, false); + } else if (info instanceof ShortcutInfo) { + ShortcutInfo st = (ShortcutInfo) info; + getTitleAndIcon(st, + st.promisedIntent != null ? st.promisedIntent : st.intent, + st.user, false); + } else if (info instanceof PackageItemInfo) { + PackageItemInfo pti = (PackageItemInfo) info; + getTitleAndIconForApp(pti.packageName, pti.user, false, pti); + } + mMainThreadExecutor.execute(new Runnable() { + + @Override + public void run() { + caller.reapplyItemInfo(info); + } + }); + } + }; + mWorkerHandler.post(request); + return new IconLoadRequest(request, mWorkerHandler); + } - application.title = entry.title; - application.iconBitmap = entry.icon; + private Bitmap getNonNullIcon(CacheEntry entry, UserHandleCompat user) { + return entry.icon == null ? getDefaultIcon(user) : entry.icon; + } + + /** + * Fill in "application" with the icon and label for "info." + */ + public synchronized void getTitleAndIcon(AppInfo application, + LauncherActivityInfoCompat info, boolean useLowResIcon) { + UserHandleCompat user = info == null ? application.user : info.getUser(); + CacheEntry entry = cacheLocked(application.componentName, info, user, + false, useLowResIcon); + application.title = Utilities.trim(entry.title); + application.iconBitmap = getNonNullIcon(entry, user); application.contentDescription = entry.contentDescription; + application.usingLowResIcon = entry.isLowResIcon; } + /** + * Updates {@param application} only if a valid entry is found. + */ + public synchronized void updateTitleAndIcon(AppInfo application) { + CacheEntry entry = cacheLocked(application.componentName, null, application.user, + false, application.usingLowResIcon); + if (entry.icon != null && !isDefaultIcon(entry.icon, application.user)) { + application.title = Utilities.trim(entry.title); + application.iconBitmap = entry.icon; + application.contentDescription = entry.contentDescription; + application.usingLowResIcon = entry.isLowResIcon; + } + } + + /** + * Returns a high res icon for the given intent and user + */ public synchronized Bitmap getIcon(Intent intent, UserHandleCompat user) { ComponentName component = intent.getComponent(); // null info means not installed, but if we have a component from the intent then @@ -246,15 +466,16 @@ public class IconCache { } LauncherActivityInfoCompat launcherActInfo = mLauncherApps.resolveActivity(intent, user); - CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, true); + CacheEntry entry = cacheLocked(component, launcherActInfo, user, true, false /* useLowRes */); return entry.icon; } /** - * Fill in "shortcutInfo" with the icon and label for "info." + * Fill in {@param shortcutInfo} with the icon and label for {@param intent}. If the + * corresponding activity is not found, it reverts to the package icon. */ public synchronized void getTitleAndIcon(ShortcutInfo shortcutInfo, Intent intent, - UserHandleCompat user, boolean usePkgIcon) { + UserHandleCompat user, boolean useLowResIcon) { ComponentName component = intent.getComponent(); // null info means not installed, but if we have a component from the intent then // we should still look in the cache for restored app icons. @@ -262,17 +483,38 @@ public class IconCache { shortcutInfo.setIcon(getDefaultIcon(user)); shortcutInfo.title = ""; shortcutInfo.usingFallbackIcon = true; + shortcutInfo.usingLowResIcon = false; } else { - LauncherActivityInfoCompat launcherActInfo = - mLauncherApps.resolveActivity(intent, user); - CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, usePkgIcon); - - shortcutInfo.setIcon(entry.icon); - shortcutInfo.title = entry.title; - shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user); + LauncherActivityInfoCompat info = mLauncherApps.resolveActivity(intent, user); + getTitleAndIcon(shortcutInfo, component, info, user, true, useLowResIcon); } } + /** + * Fill in {@param shortcutInfo} with the icon and label for {@param info} + */ + public synchronized void getTitleAndIcon( + ShortcutInfo shortcutInfo, ComponentName component, LauncherActivityInfoCompat info, + UserHandleCompat user, boolean usePkgIcon, boolean useLowResIcon) { + CacheEntry entry = cacheLocked(component, info, user, usePkgIcon, useLowResIcon); + shortcutInfo.setIcon(getNonNullIcon(entry, user)); + shortcutInfo.title = Utilities.trim(entry.title); + shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user); + shortcutInfo.usingLowResIcon = entry.isLowResIcon; + } + + /** + * Fill in {@param appInfo} with the icon and label for {@param packageName} + */ + public synchronized void getTitleAndIconForApp( + String packageName, UserHandleCompat user, boolean useLowResIcon, + PackageItemInfo infoOut) { + CacheEntry entry = getEntryForPackageLocked(packageName, user, useLowResIcon); + infoOut.iconBitmap = getNonNullIcon(entry, user); + infoOut.title = Utilities.trim(entry.title); + infoOut.usingLowResIcon = entry.isLowResIcon; + infoOut.contentDescription = entry.contentDescription; + } public synchronized Bitmap getDefaultIcon(UserHandleCompat user) { if (!mDefaultIcons.containsKey(user)) { @@ -281,16 +523,6 @@ public class IconCache { return mDefaultIcons.get(user); } - public synchronized Bitmap getIcon(ComponentName component, LauncherActivityInfoCompat info, - HashMap labelCache) { - if (info == null || component == null) { - return null; - } - - CacheEntry entry = cacheLocked(component, info, labelCache, info.getUser(), false); - return entry.icon; - } - public boolean isDefaultIcon(Bitmap icon, UserHandleCompat user) { return mDefaultIcons.get(user) == icon; } @@ -300,44 +532,27 @@ public class IconCache { * This method is not thread safe, it must be called from a synchronized method. */ private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info, - HashMap labelCache, UserHandleCompat user, boolean usePackageIcon) { - CacheKey cacheKey = new CacheKey(componentName, user); + UserHandleCompat user, boolean usePackageIcon, boolean useLowResIcon) { + ComponentKey cacheKey = new ComponentKey(componentName, user); CacheEntry entry = mCache.get(cacheKey); - if (entry == null) { + if (entry == null || (entry.isLowResIcon && !useLowResIcon)) { entry = new CacheEntry(); - mCache.put(cacheKey, entry); - if (info != null) { - ComponentName labelKey = info.getComponentName(); - if (labelCache != null && labelCache.containsKey(labelKey)) { - entry.title = labelCache.get(labelKey).toString(); - } else { - entry.title = info.getLabel().toString(); - if (labelCache != null) { - labelCache.put(labelKey, entry.title); - } - } - - entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); - entry.icon = Utilities.createIconBitmap( - info.getBadgedIcon(mIconDpi), mContext); - } else { - entry.title = ""; - Bitmap preloaded = getPreloadedIcon(componentName, user); - if (preloaded != null) { - if (DEBUG) Log.d(TAG, "using preloaded icon for " + - componentName.toShortString()); - entry.icon = preloaded; + // Check the DB first. + if (!getEntryFromDB(componentName, user, entry, useLowResIcon)) { + if (info != null) { + entry.icon = Utilities.createIconBitmap(info.getBadgedIcon(mIconDpi), mContext); } else { if (usePackageIcon) { - CacheEntry packageEntry = getEntryForPackage( - componentName.getPackageName(), user); + CacheEntry packageEntry = getEntryForPackageLocked( + componentName.getPackageName(), user, false); if (packageEntry != null) { if (DEBUG) Log.d(TAG, "using package default icon for " + componentName.toShortString()); entry.icon = packageEntry.icon; entry.title = packageEntry.title; + entry.contentDescription = packageEntry.contentDescription; } } if (entry.icon == null) { @@ -347,6 +562,11 @@ public class IconCache { } } } + + if (TextUtils.isEmpty(entry.title) && info != null) { + entry.title = info.getLabel(); + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); + } } return entry; } @@ -357,9 +577,9 @@ public class IconCache { */ public synchronized void cachePackageInstallInfo(String packageName, UserHandleCompat user, Bitmap icon, CharSequence title) { - remove(packageName, user); + removeFromMemCacheLocked(packageName, user); - CacheEntry entry = getEntryForPackage(packageName, user); + CacheEntry entry = getEntryForPackageLocked(packageName, user, false); if (!TextUtils.isEmpty(title)) { entry.title = title; } @@ -371,56 +591,68 @@ public class IconCache { /** * 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. + * */ - private CacheEntry getEntryForPackage(String packageName, UserHandleCompat user) { - ComponentName cn = new ComponentName(packageName, EMPTY_CLASS_NAME);; - CacheKey cacheKey = new CacheKey(cn, user); + private CacheEntry getEntryForPackageLocked(String packageName, UserHandleCompat user, + boolean useLowResIcon) { + ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME); + ComponentKey cacheKey = new ComponentKey(cn, user); CacheEntry entry = mCache.get(cacheKey); - if (entry == null) { + + if (entry == null || (entry.isLowResIcon && !useLowResIcon)) { entry = new CacheEntry(); - entry.title = ""; - mCache.put(cacheKey, entry); + boolean entryUpdated = true; - try { - ApplicationInfo info = mPackageManager.getApplicationInfo(packageName, 0); - entry.title = info.loadLabel(mPackageManager); - entry.icon = Utilities.createIconBitmap(info.loadIcon(mPackageManager), mContext); - } catch (NameNotFoundException e) { - if (DEBUG) Log.d(TAG, "Application not installed " + packageName); + // Check the DB first. + if (!getEntryFromDB(cn, user, entry, useLowResIcon)) { + try { + PackageInfo info = mPackageManager.getPackageInfo(packageName, 0); + ApplicationInfo appInfo = info.applicationInfo; + if (appInfo == null) { + throw new NameNotFoundException("ApplicationInfo is null"); + } + Drawable drawable = mUserManager.getBadgedDrawableForUser( + appInfo.loadIcon(mPackageManager), user); + entry.icon = Utilities.createIconBitmap(drawable, mContext); + entry.title = appInfo.loadLabel(mPackageManager); + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); + entry.isLowResIcon = false; + + // Add the icon in the DB here, since these do not get written during + // package updates. + ContentValues values = + newContentValues(entry.icon, entry.title.toString(), mPackageBgColor); + addIconToDB(values, cn, info, mUserManager.getSerialNumberForUser(user)); + + } catch (NameNotFoundException e) { + if (DEBUG) Log.d(TAG, "Application not installed " + packageName); + entryUpdated = false; + } } - if (entry.icon == null) { - entry.icon = getPreloadedIcon(cn, user); + // Only add a filled-out entry to the cache + if (entryUpdated) { + mCache.put(cacheKey, entry); } } return entry; } - public synchronized HashMap getAllIcons() { - HashMap set = new HashMap(); - for (CacheKey ck : mCache.keySet()) { - final CacheEntry e = mCache.get(ck); - set.put(ck.componentName, e.icon); - } - return set; - } - /** * Pre-load an icon into the persistent cache. * *

Queries for a component that does not exist in the package manager * will be answered by the persistent cache. * - * @param context application context * @param componentName the icon should be returned for this component * @param icon the icon to be persisted * @param dpi the native density of the icon */ - public static void preloadIcon(Context context, ComponentName componentName, Bitmap icon, - int dpi) { + public void preloadIcon(ComponentName componentName, Bitmap icon, int dpi, String label, + long userSerial) { // TODO rescale to the correct native DPI try { - PackageManager packageManager = context.getPackageManager(); + PackageManager packageManager = mContext.getPackageManager(); packageManager.getActivityIcon(componentName); // component is present on the system already, do nothing return; @@ -428,100 +660,208 @@ public class IconCache { // pass } - final String key = componentName.flattenToString(); - FileOutputStream resourceFile = null; + ContentValues values = newContentValues(icon, label, Color.TRANSPARENT); + values.put(IconDB.COLUMN_COMPONENT, componentName.flattenToString()); + values.put(IconDB.COLUMN_USER, userSerial); + mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } + + private boolean getEntryFromDB(ComponentName component, UserHandleCompat user, + CacheEntry entry, boolean lowRes) { + Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME, + new String[] {lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON, + IconDB.COLUMN_LABEL}, + IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", + new String[] {component.flattenToString(), + Long.toString(mUserManager.getSerialNumberForUser(user))}, + null, null, null); try { - resourceFile = context.openFileOutput(getResourceFilename(componentName), - Context.MODE_PRIVATE); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - if (icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 75, os)) { - byte[] buffer = os.toByteArray(); - resourceFile.write(buffer, 0, buffer.length); - } else { - Log.w(TAG, "failed to encode cache for " + key); - return; - } - } catch (FileNotFoundException e) { - Log.w(TAG, "failed to pre-load cache for " + key, e); - } catch (IOException e) { - Log.w(TAG, "failed to pre-load cache for " + key, e); - } finally { - if (resourceFile != null) { - try { - resourceFile.close(); - } catch (IOException e) { - Log.d(TAG, "failed to save restored icon for: " + key, e); + if (c.moveToNext()) { + entry.icon = loadIconNoResize(c, 0, lowRes ? mLowResOptions : null); + entry.isLowResIcon = lowRes; + entry.title = c.getString(1); + if (entry.title == null) { + entry.title = ""; + entry.contentDescription = ""; + } else { + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); } + return true; } + } finally { + c.close(); + } + return false; + } + + public static class IconLoadRequest { + private final Runnable mRunnable; + private final Handler mHandler; + + IconLoadRequest(Runnable runnable, Handler handler) { + mRunnable = runnable; + mHandler = handler; + } + + public void cancel() { + mHandler.removeCallbacks(mRunnable); } } /** - * Read a pre-loaded icon from the persistent icon cache. - * - * @param componentName the component that should own the icon - * @returns a bitmap if one is cached, or null. + * A runnable that updates invalid icons and adds missing icons in the DB for the provided + * LauncherActivityInfoCompat list. Items are updated/added one at a time, so that the + * worker thread doesn't get blocked. */ - private Bitmap getPreloadedIcon(ComponentName componentName, UserHandleCompat user) { - final String key = componentName.flattenToShortString(); - - // We don't keep icons for other profiles in persistent cache. - if (!user.equals(UserHandleCompat.myUserHandle())) { - return null; + @Thunk class SerializedIconUpdateTask implements Runnable { + private final long mUserSerial; + private final HashMap mPkgInfoMap; + private final Stack mAppsToAdd; + private final Stack mAppsToUpdate; + private final HashSet mUpdatedPackages = new HashSet(); + + @Thunk SerializedIconUpdateTask(long userSerial, HashMap pkgInfoMap, + Stack appsToAdd, + Stack appsToUpdate) { + mUserSerial = userSerial; + mPkgInfoMap = pkgInfoMap; + mAppsToAdd = appsToAdd; + mAppsToUpdate = appsToUpdate; } - if (DEBUG) Log.v(TAG, "looking for pre-load icon for " + key); - Bitmap icon = null; - FileInputStream resourceFile = null; - try { - resourceFile = mContext.openFileInput(getResourceFilename(componentName)); - byte[] buffer = new byte[1024]; - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - int bytesRead = 0; - while(bytesRead >= 0) { - bytes.write(buffer, 0, bytesRead); - bytesRead = resourceFile.read(buffer, 0, buffer.length); + @Override + public void run() { + if (!mAppsToUpdate.isEmpty()) { + LauncherActivityInfoCompat app = mAppsToUpdate.pop(); + String cn = app.getComponentName().flattenToString(); + ContentValues values = updateCacheAndGetContentValues(app, true); + mIconDb.getWritableDatabase().update(IconDB.TABLE_NAME, values, + IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", + new String[] {cn, Long.toString(mUserSerial)}); + mUpdatedPackages.add(app.getComponentName().getPackageName()); + + if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) { + // No more app to update. Notify model. + LauncherAppState.getInstance().getModel().onPackageIconsUpdated( + mUpdatedPackages, mUserManager.getUserForSerialNumber(mUserSerial)); + } + + // Let it run one more time. + scheduleNext(); + } else if (!mAppsToAdd.isEmpty()) { + LauncherActivityInfoCompat app = mAppsToAdd.pop(); + PackageInfo info = mPkgInfoMap.get(app.getComponentName().getPackageName()); + if (info != null) { + synchronized (IconCache.this) { + addIconToDBAndMemCache(app, info, mUserSerial); + } + } + + if (!mAppsToAdd.isEmpty()) { + scheduleNext(); + } } - if (DEBUG) Log.d(TAG, "read " + bytes.size()); - icon = BitmapFactory.decodeByteArray(bytes.toByteArray(), 0, bytes.size()); - if (icon == null) { - Log.w(TAG, "failed to decode pre-load icon for " + key); + } + + public void scheduleNext() { + mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN, SystemClock.uptimeMillis() + 1); + } + } + + private void updateSystemStateString() { + mSystemState = Locale.getDefault().toString(); + } + + private static final class IconDB extends SQLiteOpenHelper { + private final static int DB_VERSION = 6; + + private final static String TABLE_NAME = "icons"; + private final static String COLUMN_ROWID = "rowid"; + private final static String COLUMN_COMPONENT = "componentName"; + private final static String COLUMN_USER = "profileId"; + private final static String COLUMN_LAST_UPDATED = "lastUpdated"; + private final static String COLUMN_VERSION = "version"; + private final static String COLUMN_ICON = "icon"; + private final static String COLUMN_ICON_LOW_RES = "icon_low_res"; + private final static String COLUMN_LABEL = "label"; + private final static String COLUMN_SYSTEM_STATE = "system_state"; + + public IconDB(Context context) { + super(context, LauncherFiles.APP_ICONS_DB, null, DB_VERSION); + } + + @Override + public void onCreate(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_LOW_RES + " BLOB, " + + COLUMN_LABEL + " TEXT, " + + COLUMN_SYSTEM_STATE + " TEXT, " + + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " + + ");"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); } - } catch (FileNotFoundException e) { - if (DEBUG) Log.d(TAG, "there is no restored icon for: " + key); - } catch (IOException e) { - Log.w(TAG, "failed to read pre-load icon for: " + key, e); - } finally { - if(resourceFile != null) { - try { - resourceFile.close(); - } catch (IOException e) { - Log.d(TAG, "failed to manage pre-load icon file: " + key, e); - } + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); } } - return icon; + private void clearDB(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + onCreate(db); + } } - /** - * Remove a pre-loaded icon from the persistent icon cache. - * - * @param componentName the component that should own the icon - */ - public void deletePreloadedIcon(ComponentName componentName, UserHandleCompat user) { - // We don't keep icons for other profiles in persistent cache. - if (!user.equals(UserHandleCompat.myUserHandle()) || componentName == null) { - return; + private ContentValues newContentValues(Bitmap icon, String label, int lowResBackgroundColor) { + ContentValues values = new ContentValues(); + values.put(IconDB.COLUMN_ICON, Utilities.flattenBitmap(icon)); + + values.put(IconDB.COLUMN_LABEL, label); + values.put(IconDB.COLUMN_SYSTEM_STATE, mSystemState); + + if (lowResBackgroundColor == Color.TRANSPARENT) { + values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap( + Bitmap.createScaledBitmap(icon, + icon.getWidth() / LOW_RES_SCALE_FACTOR, + icon.getHeight() / LOW_RES_SCALE_FACTOR, true))); + } else { + synchronized (this) { + if (mLowResBitmap == null) { + mLowResBitmap = Bitmap.createBitmap(icon.getWidth() / LOW_RES_SCALE_FACTOR, + icon.getHeight() / LOW_RES_SCALE_FACTOR, Bitmap.Config.RGB_565); + mLowResCanvas = new Canvas(mLowResBitmap); + mLowResPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); + } + mLowResCanvas.drawColor(lowResBackgroundColor); + mLowResCanvas.drawBitmap(icon, new Rect(0, 0, icon.getWidth(), icon.getHeight()), + new Rect(0, 0, mLowResBitmap.getWidth(), mLowResBitmap.getHeight()), + mLowResPaint); + values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap(mLowResBitmap)); + } } - remove(componentName, user); - boolean success = mContext.deleteFile(getResourceFilename(componentName)); - if (DEBUG && success) Log.d(TAG, "removed pre-loaded icon from persistent cache"); + return values; } - private static String getResourceFilename(ComponentName component) { - String resourceName = component.flattenToShortString(); - String filename = resourceName.replace(File.separatorChar, '_'); - return RESOURCE_FILE_PREFIX + filename; + private static Bitmap loadIconNoResize(Cursor c, int iconIndex, BitmapFactory.Options options) { + byte[] data = c.getBlob(iconIndex); + try { + return BitmapFactory.decodeByteArray(data, 0, data.length, options); + } catch (Exception e) { + return null; + } } } -- cgit v1.2.3