Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6357dc10 authored by Sunny Goyal's avatar Sunny Goyal
Browse files

Fixing cache update handler deleting valid application entries

The application entries are determined by all cached entries and valid app
entries are skipped at the end
Also optimizing the update handler to perform the db lookup only once

Bug: 373085333
Test: atest IconCacheUpdateHandlerTest
Flag: EXEMPT bugfix
Change-Id: Id54b16dba1cfd5bf3989c94e02b82451d7b6d8ee
parent 4b852fef
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -294,7 +294,7 @@ public abstract class BaseIconCache {
        // Remove all active icon update tasks.
        workerHandler.removeCallbacksAndMessages(iconUpdateToken);

        return new IconCacheUpdateHandler(this);
        return new IconCacheUpdateHandler(this, mIconDb, workerHandler);
    }

    /**
@@ -546,7 +546,7 @@ public abstract class BaseIconCache {
    }

    @NonNull
    private static ComponentKey getPackageKey(@NonNull final String packageName,
    public static ComponentKey getPackageKey(@NonNull final String packageName,
            @NonNull final UserHandle user) {
        ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME);
        return new ComponentKey(cn, user);
+130 −94
Original line number Diff line number Diff line
@@ -16,24 +16,35 @@
package com.android.launcher3.icons.cache

import android.content.ComponentName
import android.database.Cursor
import android.content.pm.ApplicationInfo
import android.database.sqlite.SQLiteException
import android.os.Handler
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 com.android.launcher3.util.ComponentKey
import com.android.launcher3.util.SQLiteCacheHelper
import java.util.ArrayDeque

/** Utility class to handle updating the Icon cache */
class IconCacheUpdateHandler(private val iconCache: BaseIconCache) {
class IconCacheUpdateHandler(
    private val iconCache: BaseIconCache,
    private val cacheDb: SQLiteCacheHelper,
    private val workerHandler: Handler,
) {

    private val packagesToIgnore = ArrayMap<UserHandle, MutableSet<String>>()
    private val itemsToDelete = SparseBooleanArray()
    // Map of packageKey to ApplicationInfo, dynamically created based on all incoming data
    private val packageAppInfoMap = HashMap<ComponentKey, ApplicationInfo?>()

    private val itemsToDelete = HashSet<UpdateRow>()

    private var filterMode = MODE_SET_INVALID_ITEMS
    // During the first pass, we load all the items from DB and add all invalid items to
    // mItemsToDelete. In follow up passes, we  go through the items in mItemsToDelete, and if the
    // item is valid, removes it from the list, or leave it there.
    private var firstPass = true

    /** Sets a package to ignore for processing */
    fun addPackagesToIgnore(userHandle: UserHandle, packageName: String) {
@@ -53,51 +64,95 @@ class IconCacheUpdateHandler(private val iconCache: BaseIconCache) {
    ) {
        // Filter the list per user
        val userComponentMap = HashMap<UserHandle, HashMap<ComponentName, T>>()
        apps.forEach { app ->
        for (app in apps) {
            val userHandle = cachingLogic.getUser(app)
            var componentMap = userComponentMap.getOrPut(userHandle) { HashMap() }
            componentMap[cachingLogic.getComponent(app)] = app
            val cn = cachingLogic.getComponent(app)
            userComponentMap.getOrPut(userHandle) { HashMap() }[cn] = app

            // Populate application info map
            val packageKey = BaseIconCache.getPackageKey(cn.packageName, userHandle)
            packageAppInfoMap.getOrPut(packageKey) { cachingLogic.getApplicationInfo(app) }
        }

        for ((key, value) in userComponentMap) {
            updateIconsPerUser(key, value, cachingLogic, onUpdateCallback)
        if (firstPass) {
            userComponentMap.forEach { (user, componentMap) ->
                updateIconsPerUserForFirstPass(user, componentMap, cachingLogic, onUpdateCallback)
            }
        } else {
            userComponentMap.forEach { (user, componentMap) ->
                updateIconsPerUserForSecondPass(user, componentMap, cachingLogic, onUpdateCallback)
            }
        }

        // From now on, clear every valid item from the global valid map.
        filterMode = MODE_CLEAR_VALID_ITEMS
        firstPass = false
    }

    /**
     * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
     * the DB and are updated.
     * During the first pass, all the items from the cache are verified one-by-one and any entry
     * with no corresponding entry in {@code componentMap} is added to {@code itemsToDelete}
     *
     * @return The set of packages for which icons have updated.
     * Also starts a SerializedIconUpdateTask for all updated entries
     */
    private fun <T : Any> updateIconsPerUser(
    private fun <T : Any> updateIconsPerUserForFirstPass(
        user: UserHandle,
        componentMap: HashMap<ComponentName, T>,
        componentMap: MutableMap<ComponentName, T>,
        cachingLogic: CachingLogic<T>,
        onUpdateCallback: OnUpdateCallback,
    ) {
        var ignorePackages: Set<String> = packagesToIgnore[user] ?: emptySet()
        val userSerial = iconCache.getSerialNumberForUser(user)
        val appsToUpdate = ArrayDeque<T>()

        val userSerial = iconCache.getSerialNumberForUser(user)
        try {
            iconCache.mIconDb
            cacheDb
                .query(
                    arrayOf(
                        IconDB.COLUMN_ROWID,
                        IconDB.COLUMN_COMPONENT,
                        IconDB.COLUMN_FRESHNESS_ID,
                    ),
                    IconDB.COLUMN_USER + " = ? ",
                    "${IconDB.COLUMN_USER} = ? ",
                    arrayOf(userSerial.toString()),
                )
                .use { c ->
                    var ignorePackages = packagesToIgnore[user] ?: emptySet()

                    val indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT)
                    val indexFreshnessId = c.getColumnIndex(IconDB.COLUMN_FRESHNESS_ID)
                    val rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID)

                    while (c.moveToNext()) {
                        updateOrDeleteIcon(c, componentMap, ignorePackages, user, cachingLogic)
                            ?.let { appsToUpdate.add(it) }
                        val rowId = c.getInt(rowIndex)
                        val cn = c.getString(indexComponent)
                        val freshnessId = c.getString(indexFreshnessId) ?: ""

                        val component = ComponentName.unflattenFromString(cn)
                        if (component == null) {
                            // b/357725795
                            Log.e(TAG, "Invalid component name while updating icon cache: $cn")
                            itemsToDelete.add(
                                UpdateRow(rowId, ComponentName("", ""), user, freshnessId)
                            )
                            continue
                        }

                        val app = componentMap.remove(component)
                        if (app == null) {
                            if (!ignorePackages.contains(component.packageName)) {
                                iconCache.remove(component, user)
                                itemsToDelete.add(UpdateRow(rowId, component, user, freshnessId))
                            }
                            continue
                        }

                        if (
                            freshnessId ==
                                cachingLogic.getFreshnessIdentifier(app, iconCache.iconProvider)
                        ) {
                            // Item is up-to-date
                            continue
                        }
                        appsToUpdate.add(app)
                    }
                }
        } catch (e: SQLiteException) {
@@ -106,9 +161,8 @@ class IconCacheUpdateHandler(private val iconCache: BaseIconCache) {
        }

        // Insert remaining apps.
        if (componentMap.isNotEmpty() || !appsToUpdate.isEmpty()) {
            val appsToAdd = ArrayDeque<T>()
            appsToAdd.addAll(componentMap.values)
        if (componentMap.isNotEmpty() || appsToUpdate.isNotEmpty()) {
            val appsToAdd = ArrayDeque(componentMap.values)
            SerializedIconUpdateTask(
                    userSerial,
                    user,
@@ -122,52 +176,46 @@ class IconCacheUpdateHandler(private val iconCache: BaseIconCache) {
    }

    /**
     * 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.
     * During the second pass, we go through the items in {@code itemsToDelete}, and remove any item
     * with corresponding entry in {@code componentMap}.
     */
    @VisibleForTesting
    fun <T : Any> updateOrDeleteIcon(
        c: Cursor,
        componentMap: MutableMap<ComponentName, out T>,
        ignorePackages: Set<String>,
    private fun <T : Any> updateIconsPerUserForSecondPass(
        user: UserHandle,
        componentMap: MutableMap<ComponentName, T>,
        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)
        onUpdateCallback: OnUpdateCallback,
    ) {
        val userSerial = iconCache.getSerialNumberForUser(user)
        val appsToUpdate = ArrayDeque<T>()

        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 itr = itemsToDelete.iterator()
        while (itr.hasNext()) {
            val row = itr.next()
            if (user != row.user) continue
            val app = componentMap.remove(row.componentName) ?: continue

        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)
                }
            itr.remove()
            if (
                row.freshnessId != cachingLogic.getFreshnessIdentifier(app, iconCache.iconProvider)
            ) {
                appsToUpdate.add(app)
            }
            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
        // Insert remaining apps.
        if (componentMap.isNotEmpty() || appsToUpdate.isNotEmpty()) {
            val appsToAdd = ArrayDeque<T>()
            appsToAdd.addAll(componentMap.values)
            SerializedIconUpdateTask(
                    userSerial,
                    user,
                    appsToAdd,
                    appsToUpdate,
                    cachingLogic,
                    onUpdateCallback,
                )
                .scheduleNext()
        }

        return app
    }

    /**
@@ -175,27 +223,27 @@ class IconCacheUpdateHandler(private val iconCache: BaseIconCache) {
     * 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++
        // Ignore any application info entries which are already correct
        itemsToDelete.removeIf { row ->
            val info = packageAppInfoMap[ComponentKey(row.componentName, row.user)]
            info != null && row.freshnessId == iconCache.iconProvider.getStateForApp(info)
        }
        }
        queryBuilder.append(')')

        if (deleteCount > 0) {
            iconCache.mIconDb.delete(queryBuilder.toString(), null)
        // Commit all deletes
        if (itemsToDelete.isNotEmpty()) {
            val r = itemsToDelete.joinToString { it.rowId.toString() }
            cacheDb.delete("${IconDB.COLUMN_ROWID} IN ($r)", null)
            Log.d(TAG, "Deleting obsolete entries, count=" + itemsToDelete.size)
        }
    }

    data class UpdateRow(
        val rowId: Int,
        val componentName: ComponentName,
        val user: UserHandle,
        val freshnessId: String,
    )

    /**
     * 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
@@ -235,7 +283,7 @@ class IconCacheUpdateHandler(private val iconCache: BaseIconCache) {
        }

        fun scheduleNext() {
            iconCache.workerHandler.postAtTime(
            workerHandler.postAtTime(
                this,
                iconCache.iconUpdateToken,
                SystemClock.uptimeMillis() + 1,
@@ -243,23 +291,11 @@ class IconCacheUpdateHandler(private val iconCache: BaseIconCache) {
        }
    }

    interface OnUpdateCallback {
    fun 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
    }
}