Loading iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.javadeleted 100644 → 0 +0 −290 Original line number Diff line number Diff line /* * 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.database.Cursor; import android.database.sqlite.SQLiteException; import android.os.SystemClock; import android.os.UserHandle; import android.util.ArrayMap; import android.util.Log; import android.util.SparseBooleanArray; import androidx.annotation.VisibleForTesting; import com.android.launcher3.icons.cache.BaseIconCache.IconDB; import java.util.ArrayDeque; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; /** * 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 final BaseIconCache mIconCache; private final ArrayMap<UserHandle, Set<String>> mPackagesToIgnore = new ArrayMap<>(); private final SparseBooleanArray mItemsToDelete = new SparseBooleanArray(); private boolean mFilterMode = MODE_SET_INVALID_ITEMS; @VisibleForTesting public IconCacheUpdateHandler(BaseIconCache cache) { mIconCache = cache; } /** * Sets a package to ignore for processing */ public void addPackagesToIgnore(UserHandle userHandle, String packageName) { Set<String> packages = mPackagesToIgnore.get(userHandle); if (packages == null) { packages = new HashSet<>(); mPackagesToIgnore.put(userHandle, packages); } packages.add(packageName); } /** * 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); ArrayDeque<T> appsToUpdate = new ArrayDeque<>(); try (Cursor c = mIconCache.mIconDb.query( new String[] { IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT, IconDB.COLUMN_FRESHNESS_ID}, IconDB.COLUMN_USER + " = ? ", new String[]{Long.toString(userSerial)})) { while (c.moveToNext()) { var app = updateOrDeleteIcon(c, componentMap, ignorePackages, user, cachingLogic); if (app != null) { 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()) { ArrayDeque<T> appsToAdd = new ArrayDeque<>(); appsToAdd.addAll(componentMap.values()); new SerializedIconUpdateTask(userSerial, user, appsToAdd, appsToUpdate, cachingLogic, onUpdateCallback).scheduleNext(); } } /** * This method retrieves the component and either adds it to the list of apps to update or * adds it to a list of apps to delete from cache later. Returns the individual app if it * should be updated, or null if nothing should be updated. */ @VisibleForTesting public <T> T updateOrDeleteIcon(Cursor c, Map<ComponentName, ? extends T> componentMap, Set<String> ignorePackages, UserHandle user, CachingLogic<T> cachingLogic) { final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT); final int indexFreshnessId = c.getColumnIndex(IconDB.COLUMN_FRESHNESS_ID); final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID); int rowId = c.getInt(rowIndex); String cn = c.getString(indexComponent); ComponentName component = ComponentName.unflattenFromString(cn); if (component == null) { // b/357725795 Log.e(TAG, "Invalid component name while updating icon cache: " + cn); mItemsToDelete.put(rowId, true); return null; } T app = componentMap.remove(component); if (app == null) { if (!ignorePackages.contains(component.getPackageName())) { if (mFilterMode == MODE_SET_INVALID_ITEMS) { mIconCache.remove(component, user); mItemsToDelete.put(rowId, true); } } return null; } String freshnessId = c.getString(indexFreshnessId); if (Objects.equals(freshnessId, cachingLogic.getFreshnessIdentifier( app, mIconCache.getIconProvider()))) { if (mFilterMode == MODE_CLEAR_VALID_ITEMS) { mItemsToDelete.put(rowId, false); } return null; } return app; } /** * 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 ArrayDeque<T> mAppsToAdd; private final ArrayDeque<T> mAppsToUpdate; private final CachingLogic<T> mCachingLogic; private final HashSet<String> mUpdatedPackages = new HashSet<>(); private final OnUpdateCallback mOnUpdateCallback; SerializedIconUpdateTask(long userSerial, UserHandle userHandle, ArrayDeque<T> appsToAdd, ArrayDeque<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.removeLast(); String pkg = mCachingLogic.getComponent(app).getPackageName(); mIconCache.addIconToDBAndMemCache(app, mCachingLogic, mUserSerial); 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.removeLast(); mIconCache.addIconToDBAndMemCache(app, mCachingLogic, mUserSerial); // Let it run one more time. scheduleNext(); } } public void scheduleNext() { mIconCache.workerHandler.postAtTime(this, mIconCache.iconUpdateToken, SystemClock.uptimeMillis() + 1); } } public interface OnUpdateCallback { void onPackageIconsUpdated(HashSet<String> updatedPackages, UserHandle user); } } iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.kt 0 → 100644 +265 −0 Original line number Diff line number Diff line /* * 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.database.Cursor import android.database.sqlite.SQLiteException import android.os.SystemClock import android.os.UserHandle import android.util.ArrayMap import android.util.Log import android.util.SparseBooleanArray import androidx.annotation.VisibleForTesting import com.android.launcher3.icons.cache.BaseIconCache.IconDB import java.util.ArrayDeque /** Utility class to handle updating the Icon cache */ class IconCacheUpdateHandler(private val iconCache: BaseIconCache) { private val packagesToIgnore = ArrayMap<UserHandle, MutableSet<String>>() private val itemsToDelete = SparseBooleanArray() private var filterMode = MODE_SET_INVALID_ITEMS /** Sets a package to ignore for processing */ fun addPackagesToIgnore(userHandle: UserHandle, packageName: String) { packagesToIgnore.getOrPut(userHandle) { HashSet() }.add(packageName) } /** * 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. */ fun <T : Any> updateIcons( apps: List<T>, cachingLogic: CachingLogic<T>, onUpdateCallback: OnUpdateCallback, ) { // Filter the list per user val userComponentMap = HashMap<UserHandle, HashMap<ComponentName, T>>() apps.forEach { app -> val userHandle = cachingLogic.getUser(app) var componentMap = userComponentMap.getOrPut(userHandle) { HashMap() } componentMap[cachingLogic.getComponent(app)] = app } for ((key, value) in userComponentMap) { updateIconsPerUser(key, value, cachingLogic, onUpdateCallback) } // From now on, clear every valid item from the global valid map. filterMode = 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. */ private fun <T : Any> updateIconsPerUser( user: UserHandle, componentMap: HashMap<ComponentName, T>, cachingLogic: CachingLogic<T>, onUpdateCallback: OnUpdateCallback, ) { var ignorePackages: Set<String> = packagesToIgnore[user] ?: emptySet() val userSerial = iconCache.getSerialNumberForUser(user) val appsToUpdate = ArrayDeque<T>() try { iconCache.mIconDb .query( arrayOf( IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT, IconDB.COLUMN_FRESHNESS_ID, ), IconDB.COLUMN_USER + " = ? ", arrayOf(userSerial.toString()), ) .use { c -> while (c.moveToNext()) { updateOrDeleteIcon(c, componentMap, ignorePackages, user, cachingLogic) ?.let { appsToUpdate.add(it) } } } } catch (e: SQLiteException) { Log.d(TAG, "Error reading icon cache", e) // Continue updating whatever we have read so far } // Insert remaining apps. if (componentMap.isNotEmpty() || !appsToUpdate.isEmpty()) { val appsToAdd = ArrayDeque<T>() appsToAdd.addAll(componentMap.values) SerializedIconUpdateTask( userSerial, user, appsToAdd, appsToUpdate, cachingLogic, onUpdateCallback, ) .scheduleNext() } } /** * This method retrieves the component and either adds it to the list of apps to update or adds * it to a list of apps to delete from cache later. Returns the individual app if it should be * updated, or null if nothing should be updated. */ @VisibleForTesting fun <T : Any> updateOrDeleteIcon( c: Cursor, componentMap: MutableMap<ComponentName, out T>, ignorePackages: Set<String>, user: UserHandle, cachingLogic: CachingLogic<T>, ): T? { val indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT) val indexFreshnessId = c.getColumnIndex(IconDB.COLUMN_FRESHNESS_ID) val rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID) val rowId = c.getInt(rowIndex) val cn = c.getString(indexComponent) val component = ComponentName.unflattenFromString(cn) if (component == null) { // b/357725795 Log.e(TAG, "Invalid component name while updating icon cache: $cn") itemsToDelete.put(rowId, true) return null } val app = componentMap.remove(component) if (app == null) { if (!ignorePackages.contains(component.packageName)) { if (filterMode == MODE_SET_INVALID_ITEMS) { iconCache.remove(component, user) itemsToDelete.put(rowId, true) } } return null } val freshnessId = c.getString(indexFreshnessId) if (freshnessId == cachingLogic.getFreshnessIdentifier(app, iconCache.iconProvider)) { if (filterMode == MODE_CLEAR_VALID_ITEMS) { itemsToDelete.put(rowId, false) } return null } return app } /** * Commits all updates as part of the update handler to disk. Not more calls should be made to * this class after this. */ fun finish() { // Commit all deletes var deleteCount = 0 val queryBuilder = StringBuilder().append(IconDB.COLUMN_ROWID).append(" IN (") val count = itemsToDelete.size() for (i in 0 until count) { if (itemsToDelete.valueAt(i)) { if (deleteCount > 0) { queryBuilder.append(", ") } queryBuilder.append(itemsToDelete.keyAt(i)) deleteCount++ } } queryBuilder.append(')') if (deleteCount > 0) { iconCache.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 inner class SerializedIconUpdateTask<T : Any>( private val userSerial: Long, private val userHandle: UserHandle, private val appsToAdd: ArrayDeque<T>, private val appsToUpdate: ArrayDeque<T>, private val cachingLogic: CachingLogic<T>, private val onUpdateCallback: OnUpdateCallback, ) : Runnable { private val updatedPackages = HashSet<String>() override fun run() { if (appsToUpdate.isNotEmpty()) { val app = appsToUpdate.removeLast() val pkg = cachingLogic.getComponent(app).packageName iconCache.addIconToDBAndMemCache(app, cachingLogic, userSerial) updatedPackages.add(pkg) if (appsToUpdate.isEmpty() && updatedPackages.isNotEmpty()) { // No more app to update. Notify callback. onUpdateCallback.onPackageIconsUpdated(updatedPackages, userHandle) } // Let it run one more time. scheduleNext() } else if (appsToAdd.isNotEmpty()) { iconCache.addIconToDBAndMemCache(appsToAdd.removeLast(), cachingLogic, userSerial) // Let it run one more time. scheduleNext() } } fun scheduleNext() { iconCache.workerHandler.postAtTime( this, iconCache.iconUpdateToken, SystemClock.uptimeMillis() + 1, ) } } interface OnUpdateCallback { fun onPackageIconsUpdated(updatedPackages: HashSet<String>, user: UserHandle) } companion object { private const val TAG = "IconCacheUpdateHandler" /** * In this mode, all invalid icons are marked as to-be-deleted in [.mItemsToDelete]. This * mode is used for the first run. */ private const val MODE_SET_INVALID_ITEMS = true /** * In this mode, any valid icon is removed from [.mItemsToDelete]. This is used for all * subsequent runs, which essentially acts as set-union of all valid items. */ private const val MODE_CLEAR_VALID_ITEMS = false } } Loading
iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.javadeleted 100644 → 0 +0 −290 Original line number Diff line number Diff line /* * 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.database.Cursor; import android.database.sqlite.SQLiteException; import android.os.SystemClock; import android.os.UserHandle; import android.util.ArrayMap; import android.util.Log; import android.util.SparseBooleanArray; import androidx.annotation.VisibleForTesting; import com.android.launcher3.icons.cache.BaseIconCache.IconDB; import java.util.ArrayDeque; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; /** * 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 final BaseIconCache mIconCache; private final ArrayMap<UserHandle, Set<String>> mPackagesToIgnore = new ArrayMap<>(); private final SparseBooleanArray mItemsToDelete = new SparseBooleanArray(); private boolean mFilterMode = MODE_SET_INVALID_ITEMS; @VisibleForTesting public IconCacheUpdateHandler(BaseIconCache cache) { mIconCache = cache; } /** * Sets a package to ignore for processing */ public void addPackagesToIgnore(UserHandle userHandle, String packageName) { Set<String> packages = mPackagesToIgnore.get(userHandle); if (packages == null) { packages = new HashSet<>(); mPackagesToIgnore.put(userHandle, packages); } packages.add(packageName); } /** * 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); ArrayDeque<T> appsToUpdate = new ArrayDeque<>(); try (Cursor c = mIconCache.mIconDb.query( new String[] { IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT, IconDB.COLUMN_FRESHNESS_ID}, IconDB.COLUMN_USER + " = ? ", new String[]{Long.toString(userSerial)})) { while (c.moveToNext()) { var app = updateOrDeleteIcon(c, componentMap, ignorePackages, user, cachingLogic); if (app != null) { 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()) { ArrayDeque<T> appsToAdd = new ArrayDeque<>(); appsToAdd.addAll(componentMap.values()); new SerializedIconUpdateTask(userSerial, user, appsToAdd, appsToUpdate, cachingLogic, onUpdateCallback).scheduleNext(); } } /** * This method retrieves the component and either adds it to the list of apps to update or * adds it to a list of apps to delete from cache later. Returns the individual app if it * should be updated, or null if nothing should be updated. */ @VisibleForTesting public <T> T updateOrDeleteIcon(Cursor c, Map<ComponentName, ? extends T> componentMap, Set<String> ignorePackages, UserHandle user, CachingLogic<T> cachingLogic) { final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT); final int indexFreshnessId = c.getColumnIndex(IconDB.COLUMN_FRESHNESS_ID); final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID); int rowId = c.getInt(rowIndex); String cn = c.getString(indexComponent); ComponentName component = ComponentName.unflattenFromString(cn); if (component == null) { // b/357725795 Log.e(TAG, "Invalid component name while updating icon cache: " + cn); mItemsToDelete.put(rowId, true); return null; } T app = componentMap.remove(component); if (app == null) { if (!ignorePackages.contains(component.getPackageName())) { if (mFilterMode == MODE_SET_INVALID_ITEMS) { mIconCache.remove(component, user); mItemsToDelete.put(rowId, true); } } return null; } String freshnessId = c.getString(indexFreshnessId); if (Objects.equals(freshnessId, cachingLogic.getFreshnessIdentifier( app, mIconCache.getIconProvider()))) { if (mFilterMode == MODE_CLEAR_VALID_ITEMS) { mItemsToDelete.put(rowId, false); } return null; } return app; } /** * 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 ArrayDeque<T> mAppsToAdd; private final ArrayDeque<T> mAppsToUpdate; private final CachingLogic<T> mCachingLogic; private final HashSet<String> mUpdatedPackages = new HashSet<>(); private final OnUpdateCallback mOnUpdateCallback; SerializedIconUpdateTask(long userSerial, UserHandle userHandle, ArrayDeque<T> appsToAdd, ArrayDeque<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.removeLast(); String pkg = mCachingLogic.getComponent(app).getPackageName(); mIconCache.addIconToDBAndMemCache(app, mCachingLogic, mUserSerial); 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.removeLast(); mIconCache.addIconToDBAndMemCache(app, mCachingLogic, mUserSerial); // Let it run one more time. scheduleNext(); } } public void scheduleNext() { mIconCache.workerHandler.postAtTime(this, mIconCache.iconUpdateToken, SystemClock.uptimeMillis() + 1); } } public interface OnUpdateCallback { void onPackageIconsUpdated(HashSet<String> updatedPackages, UserHandle user); } }
iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.kt 0 → 100644 +265 −0 Original line number Diff line number Diff line /* * 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.database.Cursor import android.database.sqlite.SQLiteException import android.os.SystemClock import android.os.UserHandle import android.util.ArrayMap import android.util.Log import android.util.SparseBooleanArray import androidx.annotation.VisibleForTesting import com.android.launcher3.icons.cache.BaseIconCache.IconDB import java.util.ArrayDeque /** Utility class to handle updating the Icon cache */ class IconCacheUpdateHandler(private val iconCache: BaseIconCache) { private val packagesToIgnore = ArrayMap<UserHandle, MutableSet<String>>() private val itemsToDelete = SparseBooleanArray() private var filterMode = MODE_SET_INVALID_ITEMS /** Sets a package to ignore for processing */ fun addPackagesToIgnore(userHandle: UserHandle, packageName: String) { packagesToIgnore.getOrPut(userHandle) { HashSet() }.add(packageName) } /** * 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. */ fun <T : Any> updateIcons( apps: List<T>, cachingLogic: CachingLogic<T>, onUpdateCallback: OnUpdateCallback, ) { // Filter the list per user val userComponentMap = HashMap<UserHandle, HashMap<ComponentName, T>>() apps.forEach { app -> val userHandle = cachingLogic.getUser(app) var componentMap = userComponentMap.getOrPut(userHandle) { HashMap() } componentMap[cachingLogic.getComponent(app)] = app } for ((key, value) in userComponentMap) { updateIconsPerUser(key, value, cachingLogic, onUpdateCallback) } // From now on, clear every valid item from the global valid map. filterMode = 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. */ private fun <T : Any> updateIconsPerUser( user: UserHandle, componentMap: HashMap<ComponentName, T>, cachingLogic: CachingLogic<T>, onUpdateCallback: OnUpdateCallback, ) { var ignorePackages: Set<String> = packagesToIgnore[user] ?: emptySet() val userSerial = iconCache.getSerialNumberForUser(user) val appsToUpdate = ArrayDeque<T>() try { iconCache.mIconDb .query( arrayOf( IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT, IconDB.COLUMN_FRESHNESS_ID, ), IconDB.COLUMN_USER + " = ? ", arrayOf(userSerial.toString()), ) .use { c -> while (c.moveToNext()) { updateOrDeleteIcon(c, componentMap, ignorePackages, user, cachingLogic) ?.let { appsToUpdate.add(it) } } } } catch (e: SQLiteException) { Log.d(TAG, "Error reading icon cache", e) // Continue updating whatever we have read so far } // Insert remaining apps. if (componentMap.isNotEmpty() || !appsToUpdate.isEmpty()) { val appsToAdd = ArrayDeque<T>() appsToAdd.addAll(componentMap.values) SerializedIconUpdateTask( userSerial, user, appsToAdd, appsToUpdate, cachingLogic, onUpdateCallback, ) .scheduleNext() } } /** * This method retrieves the component and either adds it to the list of apps to update or adds * it to a list of apps to delete from cache later. Returns the individual app if it should be * updated, or null if nothing should be updated. */ @VisibleForTesting fun <T : Any> updateOrDeleteIcon( c: Cursor, componentMap: MutableMap<ComponentName, out T>, ignorePackages: Set<String>, user: UserHandle, cachingLogic: CachingLogic<T>, ): T? { val indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT) val indexFreshnessId = c.getColumnIndex(IconDB.COLUMN_FRESHNESS_ID) val rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID) val rowId = c.getInt(rowIndex) val cn = c.getString(indexComponent) val component = ComponentName.unflattenFromString(cn) if (component == null) { // b/357725795 Log.e(TAG, "Invalid component name while updating icon cache: $cn") itemsToDelete.put(rowId, true) return null } val app = componentMap.remove(component) if (app == null) { if (!ignorePackages.contains(component.packageName)) { if (filterMode == MODE_SET_INVALID_ITEMS) { iconCache.remove(component, user) itemsToDelete.put(rowId, true) } } return null } val freshnessId = c.getString(indexFreshnessId) if (freshnessId == cachingLogic.getFreshnessIdentifier(app, iconCache.iconProvider)) { if (filterMode == MODE_CLEAR_VALID_ITEMS) { itemsToDelete.put(rowId, false) } return null } return app } /** * Commits all updates as part of the update handler to disk. Not more calls should be made to * this class after this. */ fun finish() { // Commit all deletes var deleteCount = 0 val queryBuilder = StringBuilder().append(IconDB.COLUMN_ROWID).append(" IN (") val count = itemsToDelete.size() for (i in 0 until count) { if (itemsToDelete.valueAt(i)) { if (deleteCount > 0) { queryBuilder.append(", ") } queryBuilder.append(itemsToDelete.keyAt(i)) deleteCount++ } } queryBuilder.append(')') if (deleteCount > 0) { iconCache.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 inner class SerializedIconUpdateTask<T : Any>( private val userSerial: Long, private val userHandle: UserHandle, private val appsToAdd: ArrayDeque<T>, private val appsToUpdate: ArrayDeque<T>, private val cachingLogic: CachingLogic<T>, private val onUpdateCallback: OnUpdateCallback, ) : Runnable { private val updatedPackages = HashSet<String>() override fun run() { if (appsToUpdate.isNotEmpty()) { val app = appsToUpdate.removeLast() val pkg = cachingLogic.getComponent(app).packageName iconCache.addIconToDBAndMemCache(app, cachingLogic, userSerial) updatedPackages.add(pkg) if (appsToUpdate.isEmpty() && updatedPackages.isNotEmpty()) { // No more app to update. Notify callback. onUpdateCallback.onPackageIconsUpdated(updatedPackages, userHandle) } // Let it run one more time. scheduleNext() } else if (appsToAdd.isNotEmpty()) { iconCache.addIconToDBAndMemCache(appsToAdd.removeLast(), cachingLogic, userSerial) // Let it run one more time. scheduleNext() } } fun scheduleNext() { iconCache.workerHandler.postAtTime( this, iconCache.iconUpdateToken, SystemClock.uptimeMillis() + 1, ) } } interface OnUpdateCallback { fun onPackageIconsUpdated(updatedPackages: HashSet<String>, user: UserHandle) } companion object { private const val TAG = "IconCacheUpdateHandler" /** * In this mode, all invalid icons are marked as to-be-deleted in [.mItemsToDelete]. This * mode is used for the first run. */ private const val MODE_SET_INVALID_ITEMS = true /** * In this mode, any valid icon is removed from [.mItemsToDelete]. This is used for all * subsequent runs, which essentially acts as set-union of all valid items. */ private const val MODE_CLEAR_VALID_ITEMS = false } }