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

Commit f2efb870 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Moving IconCacheUpdateHandler to kotlin" into main

parents 0f1a3800 bd9e4ecd
Loading
Loading
Loading
Loading
+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);
    }
}
+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
    }
}