Convert TaskIconCache and TaskThumbnailCache to kotlin

Bug: 369590189
Flag: com.android.launcher3.enable_refactor_task_thumbnail
Test: Presubmits
Change-Id: I1becdcdf2fffd2c1fe548e7f182a7fd851be3c08
diff --git a/quickstep/src/com/android/quickstep/HighResLoadingState.kt b/quickstep/src/com/android/quickstep/HighResLoadingState.kt
new file mode 100644
index 0000000..8a21c4f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/HighResLoadingState.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2025 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.quickstep
+
+import android.content.res.Resources
+import com.android.quickstep.recents.data.HighResLoadingStateNotifier
+
+/** Determines when high res or low res thumbnails should be loaded. */
+class HighResLoadingState : HighResLoadingStateNotifier {
+    // If the device does not support low-res thumbnails, only attempt to load high-res thumbnails
+    private val forceHighResThumbnails = !supportsLowResThumbnails()
+    var visible: Boolean = false
+        set(value) {
+            field = value
+            updateState()
+        }
+
+    var flingingFast = false
+        set(value) {
+            field = value
+            updateState()
+        }
+
+    var isEnabled: Boolean = false
+        private set
+
+    private val callbacks = ArrayList<HighResLoadingStateChangedCallback>()
+
+    interface HighResLoadingStateChangedCallback {
+        fun onHighResLoadingStateChanged(enabled: Boolean)
+    }
+
+    override fun addCallback(callback: HighResLoadingStateChangedCallback) {
+        callbacks.add(callback)
+    }
+
+    override fun removeCallback(callback: HighResLoadingStateChangedCallback) {
+        callbacks.remove(callback)
+    }
+
+    private fun updateState() {
+        val prevState = isEnabled
+        isEnabled = forceHighResThumbnails || (visible && !flingingFast)
+        if (prevState != isEnabled) {
+            for (callback in callbacks.asReversed()) {
+                callback.onHighResLoadingStateChanged(isEnabled)
+            }
+        }
+    }
+
+    /**
+     * Returns Whether device supports low-res thumbnails. Low-res files are an optimization for
+     * faster load times of snapshots. Devices can optionally disable low-res files so that they
+     * only store snapshots at high-res scale. The actual scale can be configured in frameworks/base
+     * config overlay.
+     */
+    private fun supportsLowResThumbnails(): Boolean {
+        val res = Resources.getSystem()
+        val resId = res.getIdentifier("config_lowResTaskSnapshotScale", "dimen", "android")
+        if (resId != 0) {
+            return 0 < res.getFloat(resId)
+        }
+        return true
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
deleted file mode 100644
index c4221a1..0000000
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ /dev/null
@@ -1,308 +0,0 @@
-/*
- * 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.quickstep;
-
-import static com.android.launcher3.Flags.enableOverviewIconMenu;
-import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.app.ActivityManager.TaskDescription;
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.UserHandle;
-import android.text.TextUtils;
-import android.util.SparseArray;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.icons.BaseIconFactory;
-import com.android.launcher3.icons.BaseIconFactory.IconOptions;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.IconProvider;
-import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.CancellableTask;
-import com.android.launcher3.util.DisplayController;
-import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
-import com.android.launcher3.util.DisplayController.Info;
-import com.android.launcher3.util.FlagOp;
-import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.task.thumbnail.data.TaskIconDataSource;
-import com.android.quickstep.util.TaskKeyLruCache;
-import com.android.quickstep.util.TaskVisualsChangeListener;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.Task.TaskKey;
-import com.android.systemui.shared.system.PackageManagerWrapper;
-
-import java.util.concurrent.Executor;
-
-/**
- * Manages the caching of task icons and related data.
- */
-public class TaskIconCache implements TaskIconDataSource, DisplayInfoChangeListener {
-
-    private final Executor mBgExecutor;
-
-    private final Context mContext;
-    private final TaskKeyLruCache<TaskCacheEntry> mIconCache;
-    private final SparseArray<BitmapInfo> mDefaultIcons = new SparseArray<>();
-    private BitmapInfo mDefaultIconBase = null;
-
-    private final IconProvider mIconProvider;
-
-    private BaseIconFactory mIconFactory;
-
-    @Nullable
-    public TaskVisualsChangeListener mTaskVisualsChangeListener = null;
-
-    public TaskIconCache(Context context, Executor bgExecutor, IconProvider iconProvider) {
-        mContext = context;
-        mBgExecutor = bgExecutor;
-        mIconProvider = iconProvider;
-
-        Resources res = context.getResources();
-        int cacheSize = res.getInteger(R.integer.recentsIconCacheSize);
-
-        mIconCache = new TaskKeyLruCache<>(cacheSize);
-
-        DisplayController.INSTANCE.get(mContext).addChangeListener(this);
-    }
-
-    @Override
-    public void onDisplayInfoChanged(Context context, Info info, int flags) {
-        if ((flags & CHANGE_DENSITY) != 0) {
-            clearCache();
-        }
-    }
-
-    /**
-     * Asynchronously fetches the icon and other task data.
-     *
-     * @param task The task to fetch the data for
-     * @param callback The callback to receive the task after its data has been populated.
-     * @return A cancelable handle to the request
-     */
-    @Override
-    public CancellableTask getIconInBackground(Task task, @NonNull GetTaskIconCallback callback) {
-        Preconditions.assertUIThread();
-        if (task.icon != null) {
-            // Nothing to load, the icon is already loaded
-            callback.onTaskIconReceived(task.icon, task.titleDescription, task.title);
-            return null;
-        }
-        CancellableTask<TaskCacheEntry> request = new CancellableTask<>(
-                () -> getCacheEntry(task),
-                MAIN_EXECUTOR,
-                result -> {
-                    task.icon = result.icon;
-                    task.titleDescription = result.contentDescription;
-                    task.title = result.title;
-
-                    callback.onTaskIconReceived(
-                            result.icon,
-                            result.contentDescription,
-                            result.title);
-                    dispatchIconUpdate(task.key.id);
-                }
-        );
-        mBgExecutor.execute(request);
-        return request;
-    }
-
-    /**
-     * Clears the icon cache
-     */
-    public void clearCache() {
-        mBgExecutor.execute(this::resetFactory);
-    }
-
-    void onTaskRemoved(TaskKey taskKey) {
-        mIconCache.remove(taskKey);
-    }
-
-    void invalidateCacheEntries(String pkg, UserHandle handle) {
-        mBgExecutor.execute(() -> mIconCache.removeAll(key ->
-                pkg.equals(key.getPackageName()) && handle.getIdentifier() == key.userId));
-    }
-
-    @WorkerThread
-    private TaskCacheEntry getCacheEntry(Task task) {
-        TaskCacheEntry entry = mIconCache.getAndInvalidateIfModified(task.key);
-        if (entry != null) {
-            return entry;
-        }
-
-        TaskDescription desc = task.taskDescription;
-        TaskKey key = task.key;
-        ActivityInfo activityInfo = null;
-
-        // Create new cache entry
-        entry = new TaskCacheEntry();
-
-        // Load icon
-        // TODO: Load icon resource (b/143363444)
-        Bitmap icon = getIcon(desc, key.userId);
-        if (icon != null) {
-            entry.icon = getBitmapInfo(
-                    new BitmapDrawable(mContext.getResources(), icon),
-                    key.userId,
-                    desc.getPrimaryColor(),
-                    false /* isInstantApp */).newIcon(mContext);
-        } else {
-            activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
-                    key.getComponent(), key.userId);
-            if (activityInfo != null) {
-                BitmapInfo bitmapInfo = getBitmapInfo(
-                        mIconProvider.getIcon(activityInfo),
-                        key.userId,
-                        desc.getPrimaryColor(),
-                        activityInfo.applicationInfo.isInstantApp());
-                entry.icon = bitmapInfo.newIcon(mContext);
-            } else {
-                entry.icon = getDefaultIcon(key.userId);
-            }
-        }
-
-        // Skip loading the content description if the activity no longer exists
-        if (activityInfo == null) {
-            activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
-                    key.getComponent(), key.userId);
-        }
-        if (activityInfo != null) {
-            entry.contentDescription = getBadgedContentDescription(
-                    activityInfo, task.key.userId, task.taskDescription);
-            if (enableOverviewIconMenu()) {
-                entry.title = Utilities.trim(activityInfo.loadLabel(mContext.getPackageManager()));
-            }
-        }
-
-        mIconCache.put(task.key, entry);
-        return entry;
-    }
-
-    private Bitmap getIcon(ActivityManager.TaskDescription desc, int userId) {
-        if (desc.getInMemoryIcon() != null) {
-            return desc.getInMemoryIcon();
-        }
-        return ActivityManager.TaskDescription.loadTaskDescriptionIcon(
-                desc.getIconFilename(), userId);
-    }
-
-    private String getBadgedContentDescription(ActivityInfo info, int userId, TaskDescription td) {
-        PackageManager pm = mContext.getPackageManager();
-        String taskLabel = td == null ? null : Utilities.trim(td.getLabel());
-        if (TextUtils.isEmpty(taskLabel)) {
-            taskLabel = Utilities.trim(info.loadLabel(pm));
-        }
-
-        String applicationLabel = Utilities.trim(info.applicationInfo.loadLabel(pm));
-        String badgedApplicationLabel = userId != UserHandle.myUserId()
-                ? pm.getUserBadgedLabel(applicationLabel, UserHandle.of(userId)).toString()
-                : applicationLabel;
-        return applicationLabel.equals(taskLabel)
-                ? badgedApplicationLabel : badgedApplicationLabel + " " + taskLabel;
-    }
-
-    @WorkerThread
-    private Drawable getDefaultIcon(int userId) {
-        synchronized (mDefaultIcons) {
-            if (mDefaultIconBase == null) {
-                try (BaseIconFactory bif = getIconFactory()) {
-                    mDefaultIconBase = bif.makeDefaultIcon(mIconProvider);
-                }
-            }
-
-            int index;
-            if ((index = mDefaultIcons.indexOfKey(userId)) >= 0) {
-                return mDefaultIcons.valueAt(index).newIcon(mContext);
-            } else {
-                BitmapInfo info = mDefaultIconBase.withFlags(
-                        UserCache.INSTANCE.get(mContext).getUserInfo(UserHandle.of(userId))
-                                .applyBitmapInfoFlags(FlagOp.NO_OP));
-                mDefaultIcons.put(userId, info);
-                return info.newIcon(mContext);
-            }
-        }
-    }
-
-    @WorkerThread
-    private BitmapInfo getBitmapInfo(Drawable drawable, int userId,
-            int primaryColor, boolean isInstantApp) {
-        try (BaseIconFactory bif = getIconFactory()) {
-            bif.setWrapperBackgroundColor(primaryColor);
-
-            // User version code O, so that the icon is always wrapped in an adaptive icon container
-            return bif.createBadgedIconBitmap(drawable,
-                    new IconOptions()
-                            .setUser(UserCache.INSTANCE.get(mContext)
-                                    .getUserInfo(UserHandle.of(userId)))
-                            .setInstantApp(isInstantApp)
-                            .setExtractedColor(0));
-        }
-    }
-
-    @WorkerThread
-    private BaseIconFactory getIconFactory() {
-        if (mIconFactory == null) {
-            mIconFactory = new BaseIconFactory(mContext,
-                    DisplayController.INSTANCE.get(mContext).getInfo().getDensityDpi(),
-                    mContext.getResources().getDimensionPixelSize(
-                            R.dimen.task_icon_cache_default_icon_size));
-        }
-        return mIconFactory;
-    }
-
-    @WorkerThread
-    private void resetFactory() {
-        mIconFactory = null;
-        mIconCache.evictAll();
-    }
-
-    private static class TaskCacheEntry {
-        public Drawable icon;
-        public String contentDescription = "";
-        public String title = "";
-    }
-
-    /** Callback used when retrieving app icons from cache. */
-    public interface GetTaskIconCallback {
-        /** Called when task icon is retrieved. */
-        void onTaskIconReceived(Drawable icon, String contentDescription, String title);
-    }
-
-    void registerTaskVisualsChangeListener(TaskVisualsChangeListener newListener) {
-        mTaskVisualsChangeListener = newListener;
-    }
-
-    void removeTaskVisualsChangeListener() {
-        mTaskVisualsChangeListener = null;
-    }
-
-    void dispatchIconUpdate(int taskId) {
-        if (mTaskVisualsChangeListener != null) {
-            mTaskVisualsChangeListener.onTaskIconChanged(taskId);
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.kt b/quickstep/src/com/android/quickstep/TaskIconCache.kt
new file mode 100644
index 0000000..bb0a304
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.kt
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2025 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.quickstep
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.util.SparseArray
+import androidx.annotation.WorkerThread
+import com.android.launcher3.Flags.enableOverviewIconMenu
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.icons.BaseIconFactory
+import com.android.launcher3.icons.BaseIconFactory.IconOptions
+import com.android.launcher3.icons.BitmapInfo
+import com.android.launcher3.icons.IconProvider
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.CancellableTask
+import com.android.launcher3.util.DisplayController
+import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.FlagOp
+import com.android.launcher3.util.Preconditions
+import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
+import com.android.quickstep.util.TaskKeyLruCache
+import com.android.quickstep.util.TaskVisualsChangeListener
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.android.systemui.shared.system.PackageManagerWrapper
+import java.util.concurrent.Executor
+
+/** Manages the caching of task icons and related data. */
+class TaskIconCache(
+    private val context: Context,
+    private val bgExecutor: Executor,
+    private val iconProvider: IconProvider,
+) : TaskIconDataSource, DisplayInfoChangeListener {
+    private val iconCache =
+        TaskKeyLruCache<TaskCacheEntry>(
+            context.resources.getInteger(R.integer.recentsIconCacheSize)
+        )
+    private val defaultIcons = SparseArray<BitmapInfo>()
+    private var defaultIconBase: BitmapInfo? = null
+
+    private var _iconFactory: BaseIconFactory? = null
+    @get:WorkerThread
+    private val iconFactory: BaseIconFactory
+        get() =
+            _iconFactory
+                ?: BaseIconFactory(
+                        context,
+                        DisplayController.INSTANCE[context].info.densityDpi,
+                        context.resources.getDimensionPixelSize(
+                            R.dimen.task_icon_cache_default_icon_size
+                        ),
+                    )
+                    .also { _iconFactory = it }
+
+    var taskVisualsChangeListener: TaskVisualsChangeListener? = null
+
+    init {
+        DisplayController.INSTANCE.get(context).addChangeListener(this)
+    }
+
+    override fun onDisplayInfoChanged(context: Context, info: DisplayController.Info, flags: Int) {
+        if ((flags and DisplayController.CHANGE_DENSITY) != 0) {
+            clearCache()
+        }
+    }
+
+    /**
+     * Asynchronously fetches the icon and other task data.
+     *
+     * @param task The task to fetch the data for
+     * @param callback The callback to receive the task after its data has been populated.
+     * @return A cancelable handle to the request
+     */
+    override fun getIconInBackground(
+        task: Task,
+        callback: GetTaskIconCallback,
+    ): CancellableTask<*>? {
+        Preconditions.assertUIThread()
+        if (task.icon != null) {
+            // Nothing to load, the icon is already loaded
+            callback.onTaskIconReceived(task.icon, task.titleDescription ?: "", task.title ?: "")
+            return null
+        }
+        val request =
+            CancellableTask(
+                { getCacheEntry(task) },
+                Executors.MAIN_EXECUTOR,
+                { result: TaskCacheEntry ->
+                    task.icon = result.icon
+                    task.titleDescription = result.contentDescription
+                    task.title = result.title
+
+                    callback.onTaskIconReceived(
+                        result.icon,
+                        result.contentDescription,
+                        result.title,
+                    )
+                    dispatchIconUpdate(task.key.id)
+                },
+            )
+        bgExecutor.execute(request)
+        return request
+    }
+
+    /** Clears the icon cache */
+    fun clearCache() {
+        bgExecutor.execute { resetFactory() }
+    }
+
+    fun onTaskRemoved(taskKey: TaskKey) {
+        iconCache.remove(taskKey)
+    }
+
+    fun invalidateCacheEntries(pkg: String, handle: UserHandle) {
+        bgExecutor.execute {
+            iconCache.removeAll { key: TaskKey ->
+                pkg == key.packageName && handle.identifier == key.userId
+            }
+        }
+    }
+
+    @WorkerThread
+    private fun getCacheEntry(task: Task): TaskCacheEntry {
+        var entry = iconCache.getAndInvalidateIfModified(task.key)
+        if (entry != null) {
+            return entry
+        }
+
+        val desc = task.taskDescription
+        val key = task.key
+        var activityInfo: ActivityInfo? = null
+
+        // Create new cache entry
+        entry = TaskCacheEntry()
+
+        // Load icon
+        val icon = getIcon(desc, key.userId)
+        entry.icon =
+            if (icon != null) {
+                getBitmapInfo(
+                        BitmapDrawable(context.resources, icon),
+                        key.userId,
+                        desc.primaryColor,
+                        false, /* isInstantApp */
+                    )
+                    .newIcon(context)
+            } else {
+                activityInfo =
+                    PackageManagerWrapper.getInstance().getActivityInfo(key.component, key.userId)
+                if (activityInfo != null) {
+                    val bitmapInfo =
+                        getBitmapInfo(
+                            iconProvider.getIcon(activityInfo),
+                            key.userId,
+                            desc.primaryColor,
+                            activityInfo.applicationInfo.isInstantApp,
+                        )
+                    bitmapInfo.newIcon(context)
+                } else {
+                    getDefaultIcon(key.userId)
+                }
+            }
+
+        // Skip loading the content description if the activity no longer exists
+        activityInfo =
+            activityInfo
+                ?: PackageManagerWrapper.getInstance().getActivityInfo(key.component, key.userId)
+
+        if (activityInfo != null) {
+            entry.contentDescription =
+                getBadgedContentDescription(activityInfo, task.key.userId, task.taskDescription)
+            if (enableOverviewIconMenu()) {
+                entry.title = Utilities.trim(activityInfo.loadLabel(context.packageManager))
+            }
+        }
+
+        iconCache.put(task.key, entry)
+        return entry
+    }
+
+    private fun getIcon(desc: ActivityManager.TaskDescription, userId: Int): Bitmap? =
+        desc.inMemoryIcon
+            ?: ActivityManager.TaskDescription.loadTaskDescriptionIcon(desc.iconFilename, userId)
+
+    private fun getBadgedContentDescription(
+        info: ActivityInfo,
+        userId: Int,
+        taskDescription: ActivityManager.TaskDescription?,
+    ): String {
+        val packageManager = context.packageManager
+        var taskLabel = taskDescription?.let { Utilities.trim(it.label) }
+        if (taskLabel.isNullOrEmpty()) {
+            taskLabel = Utilities.trim(info.loadLabel(packageManager))
+        }
+
+        val applicationLabel = Utilities.trim(info.applicationInfo.loadLabel(packageManager))
+        val badgedApplicationLabel =
+            if (userId != UserHandle.myUserId())
+                packageManager
+                    .getUserBadgedLabel(applicationLabel, UserHandle.of(userId))
+                    .toString()
+            else applicationLabel
+        return if (applicationLabel == taskLabel) badgedApplicationLabel
+        else "$badgedApplicationLabel $taskLabel"
+    }
+
+    @WorkerThread
+    private fun getDefaultIcon(userId: Int): Drawable {
+        synchronized(defaultIcons) {
+            val defaultIconBase =
+                defaultIconBase ?: iconFactory.use { it.makeDefaultIcon(iconProvider) }
+            val index: Int = defaultIcons.indexOfKey(userId)
+            return if (index >= 0) {
+                defaultIcons.valueAt(index).newIcon(context)
+            } else {
+                val info =
+                    defaultIconBase.withFlags(
+                        UserCache.INSTANCE.get(context)
+                            .getUserInfo(UserHandle.of(userId))
+                            .applyBitmapInfoFlags(FlagOp.NO_OP)
+                    )
+                defaultIcons.put(userId, info)
+                info.newIcon(context)
+            }
+        }
+    }
+
+    @WorkerThread
+    private fun getBitmapInfo(
+        drawable: Drawable,
+        userId: Int,
+        primaryColor: Int,
+        isInstantApp: Boolean,
+    ): BitmapInfo {
+        iconFactory.use { iconFactory ->
+            iconFactory.setWrapperBackgroundColor(primaryColor)
+            // User version code O, so that the icon is always wrapped in an adaptive icon container
+            return iconFactory.createBadgedIconBitmap(
+                drawable,
+                IconOptions()
+                    .setUser(UserCache.INSTANCE.get(context).getUserInfo(UserHandle.of(userId)))
+                    .setInstantApp(isInstantApp)
+                    .setExtractedColor(0),
+            )
+        }
+    }
+
+    @WorkerThread
+    private fun resetFactory() {
+        _iconFactory = null
+        iconCache.evictAll()
+    }
+
+    private data class TaskCacheEntry(
+        var icon: Drawable? = null,
+        var contentDescription: String = "",
+        var title: String = "",
+    )
+
+    /** Callback used when retrieving app icons from cache. */
+    fun interface GetTaskIconCallback {
+        /** Called when task icon is retrieved. */
+        fun onTaskIconReceived(icon: Drawable?, contentDescription: String, title: String)
+    }
+
+    fun registerTaskVisualsChangeListener(newListener: TaskVisualsChangeListener?) {
+        taskVisualsChangeListener = newListener
+    }
+
+    fun removeTaskVisualsChangeListener() {
+        taskVisualsChangeListener = null
+    }
+
+    private fun dispatchIconUpdate(taskId: Int) {
+        taskVisualsChangeListener?.onTaskIconChanged(taskId)
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
deleted file mode 100644
index 580dcc2..0000000
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- * 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.quickstep;
-
-import static com.android.launcher3.Flags.enableGridOnlyOverview;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.content.Context;
-import android.content.res.Resources;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.launcher3.R;
-import com.android.launcher3.util.CancellableTask;
-import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.recents.data.HighResLoadingStateNotifier;
-import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource;
-import com.android.quickstep.util.TaskKeyByLastActiveTimeCache;
-import com.android.quickstep.util.TaskKeyCache;
-import com.android.quickstep.util.TaskKeyLruCache;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.Task.TaskKey;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-
-import java.util.ArrayList;
-import java.util.concurrent.Executor;
-import java.util.function.Consumer;
-
-public class TaskThumbnailCache implements TaskThumbnailDataSource {
-
-    private final Executor mBgExecutor;
-    private final TaskKeyCache<ThumbnailData> mCache;
-    private final HighResLoadingState mHighResLoadingState;
-    private final boolean mEnableTaskSnapshotPreloading;
-    private final Context mContext;
-
-    public static class HighResLoadingState implements HighResLoadingStateNotifier {
-        private boolean mForceHighResThumbnails;
-        private boolean mVisible;
-        private boolean mFlingingFast;
-        private boolean mHighResLoadingEnabled;
-        private ArrayList<HighResLoadingStateChangedCallback> mCallbacks = new ArrayList<>();
-
-        public interface HighResLoadingStateChangedCallback {
-            void onHighResLoadingStateChanged(boolean enabled);
-        }
-
-        private HighResLoadingState(Context context) {
-            // If the device does not support low-res thumbnails, only attempt to load high-res
-            // thumbnails
-            mForceHighResThumbnails = !supportsLowResThumbnails();
-        }
-
-        @Override
-        public void addCallback(@NonNull HighResLoadingStateChangedCallback callback) {
-            mCallbacks.add(callback);
-        }
-
-        @Override
-        public void removeCallback(@NonNull HighResLoadingStateChangedCallback callback) {
-            mCallbacks.remove(callback);
-        }
-
-        public void setVisible(boolean visible) {
-            mVisible = visible;
-            updateState();
-        }
-
-        public void setFlingingFast(boolean flingingFast) {
-            mFlingingFast = flingingFast;
-            updateState();
-        }
-
-        public boolean isEnabled() {
-            return mHighResLoadingEnabled;
-        }
-
-        private void updateState() {
-            boolean prevState = mHighResLoadingEnabled;
-            mHighResLoadingEnabled = mForceHighResThumbnails || (mVisible && !mFlingingFast);
-            if (prevState != mHighResLoadingEnabled) {
-                for (int i = mCallbacks.size() - 1; i >= 0; i--) {
-                    mCallbacks.get(i).onHighResLoadingStateChanged(mHighResLoadingEnabled);
-                }
-            }
-        }
-    }
-
-    public TaskThumbnailCache(Context context, Executor bgExecutor) {
-        this(context, bgExecutor,
-                context.getResources().getInteger(R.integer.recentsThumbnailCacheSize));
-    }
-
-    private TaskThumbnailCache(Context context, Executor bgExecutor, int cacheSize) {
-        this(context, bgExecutor,
-                enableGridOnlyOverview() ? new TaskKeyByLastActiveTimeCache<>(cacheSize)
-                        : new TaskKeyLruCache<>(cacheSize));
-    }
-
-    @VisibleForTesting
-    TaskThumbnailCache(Context context, Executor bgExecutor, TaskKeyCache<ThumbnailData> cache) {
-        mBgExecutor = bgExecutor;
-        mHighResLoadingState = new HighResLoadingState(context);
-        mContext = context;
-
-        Resources res = context.getResources();
-        mEnableTaskSnapshotPreloading = res.getBoolean(R.bool.config_enableTaskSnapshotPreloading);
-        mCache = cache;
-    }
-
-    /**
-     * Synchronously fetches the thumbnail for the given task at the specified resolution level, and
-     * puts it in the cache.
-     */
-    public void updateThumbnailInCache(Task task, boolean lowResolution) {
-        if (task == null) {
-            return;
-        }
-        Preconditions.assertUIThread();
-        // Fetch the thumbnail for this task and put it in the cache
-        if (task.thumbnail == null) {
-            getThumbnailInBackground(task.key, lowResolution, t -> task.thumbnail = t);
-        }
-    }
-
-    /**
-     * Synchronously updates the thumbnail in the cache if it is already there.
-     */
-    public void updateTaskSnapShot(int taskId, ThumbnailData thumbnail) {
-        Preconditions.assertUIThread();
-        mCache.updateIfAlreadyInCache(taskId, thumbnail);
-    }
-
-    /**
-     * Asynchronously fetches the thumbnail for the given {@code task}.
-     *
-     * @param callback The callback to receive the task after its data has been populated.
-     * @return A cancelable handle to the request
-     */
-    @Override
-    public CancellableTask<ThumbnailData> getThumbnailInBackground(
-            Task task, @NonNull Consumer<ThumbnailData> callback) {
-        Preconditions.assertUIThread();
-
-        boolean lowResolution = !mHighResLoadingState.isEnabled();
-        if (task.thumbnail != null && task.thumbnail.getThumbnail() != null
-                && (!task.thumbnail.reducedResolution || lowResolution)) {
-            // Nothing to load, the thumbnail is already high-resolution or matches what the
-            // request, so just callback
-            callback.accept(task.thumbnail);
-            return null;
-        }
-
-        return getThumbnailInBackground(task.key, !mHighResLoadingState.isEnabled(), callback);
-    }
-
-    /**
-     * Updates cache size and remove excess entries if current size is more than new cache size.
-     *
-     * @return whether cache size has increased
-     */
-    public boolean updateCacheSizeAndRemoveExcess() {
-        int newSize = mContext.getResources().getInteger(R.integer.recentsThumbnailCacheSize);
-        int oldSize = mCache.getMaxSize();
-        if (newSize == oldSize) {
-            // Return if no change in size
-            return false;
-        }
-
-        mCache.updateCacheSizeAndRemoveExcess(newSize);
-        return newSize > oldSize;
-    }
-
-    private CancellableTask<ThumbnailData> getThumbnailInBackground(TaskKey key,
-            boolean lowResolution, Consumer<ThumbnailData> callback) {
-        Preconditions.assertUIThread();
-
-        ThumbnailData cachedThumbnail = mCache.getAndInvalidateIfModified(key);
-        if (cachedThumbnail != null &&  cachedThumbnail.getThumbnail() != null
-                && (!cachedThumbnail.reducedResolution || lowResolution)) {
-            // Already cached, lets use that thumbnail
-            callback.accept(cachedThumbnail);
-            return null;
-        }
-
-        CancellableTask<ThumbnailData> request = new CancellableTask<>(
-                () -> {
-                    ThumbnailData thumbnailData = ActivityManagerWrapper.getInstance()
-                            .getTaskThumbnail(key.id, lowResolution);
-                    return thumbnailData.getThumbnail() != null ? thumbnailData
-                            : ActivityManagerWrapper.getInstance().takeTaskThumbnail(key.id);
-                },
-                MAIN_EXECUTOR,
-                result -> {
-                    // Avoid an async timing issue that a low res entry replaces an existing high
-                    // res entry in high res enabled state, so we check before putting it to cache
-                    if (enableGridOnlyOverview() && result.reducedResolution
-                            && getHighResLoadingState().isEnabled()) {
-                        ThumbnailData newCachedThumbnail = mCache.getAndInvalidateIfModified(key);
-                        if (newCachedThumbnail != null && newCachedThumbnail.getThumbnail() != null
-                                && !newCachedThumbnail.reducedResolution) {
-                            return;
-                        }
-                    }
-                    mCache.put(key, result);
-                    callback.accept(result);
-                }
-        );
-        mBgExecutor.execute(request);
-        return request;
-    }
-
-    /**
-     * Clears the cache.
-     */
-    public void clear() {
-        mCache.evictAll();
-    }
-
-    /**
-     * Removes the cached thumbnail for the given task.
-     */
-    public void remove(Task.TaskKey key) {
-        mCache.remove(key);
-    }
-
-    /**
-     * @return The cache size.
-     */
-    public int getCacheSize() {
-        return mCache.getMaxSize();
-    }
-
-    /**
-     * @return The mutable high-res loading state.
-     */
-    public HighResLoadingState getHighResLoadingState() {
-        return mHighResLoadingState;
-    }
-
-    /**
-     * @return Whether to enable background preloading of task thumbnails.
-     */
-    public boolean isPreloadingEnabled() {
-        return mEnableTaskSnapshotPreloading && mHighResLoadingState.mVisible;
-    }
-
-    /**
-     * @return Whether device supports low-res thumbnails. Low-res files are an optimization
-     * for faster load times of snapshots. Devices can optionally disable low-res files so that
-     * they only store snapshots at high-res scale. The actual scale can be configured in
-     * frameworks/base config overlay.
-     */
-    private static boolean supportsLowResThumbnails() {
-        Resources res = Resources.getSystem();
-        int resId = res.getIdentifier("config_lowResTaskSnapshotScale", "dimen", "android");
-        if (resId != 0) {
-            return 0 < res.getFloat(resId);
-        }
-        return true;
-    }
-
-}
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.kt b/quickstep/src/com/android/quickstep/TaskThumbnailCache.kt
new file mode 100644
index 0000000..7de4481
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2025 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.quickstep
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.Flags.enableGridOnlyOverview
+import com.android.launcher3.R
+import com.android.launcher3.util.CancellableTask
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.Preconditions
+import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
+import com.android.quickstep.util.TaskKeyByLastActiveTimeCache
+import com.android.quickstep.util.TaskKeyCache
+import com.android.quickstep.util.TaskKeyLruCache
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+
+class TaskThumbnailCache
+@VisibleForTesting
+internal constructor(
+    private val context: Context,
+    private val bgExecutor: Executor,
+    private val cache: TaskKeyCache<ThumbnailData>,
+) : TaskThumbnailDataSource {
+    val highResLoadingState = HighResLoadingState()
+    private val enableTaskSnapshotPreloading =
+        context.resources.getBoolean(R.bool.config_enableTaskSnapshotPreloading)
+
+    @JvmOverloads
+    constructor(
+        context: Context,
+        bgExecutor: Executor,
+        cacheSize: Int = context.resources.getInteger(R.integer.recentsThumbnailCacheSize),
+    ) : this(
+        context,
+        bgExecutor,
+        if (enableGridOnlyOverview()) TaskKeyByLastActiveTimeCache(cacheSize)
+        else TaskKeyLruCache(cacheSize),
+    )
+
+    /**
+     * Synchronously fetches the thumbnail for the given task at the specified resolution level, and
+     * puts it in the cache.
+     */
+    fun updateThumbnailInCache(task: Task?, lowResolution: Boolean) {
+        task ?: return
+
+        Preconditions.assertUIThread()
+        // Fetch the thumbnail for this task and put it in the cache
+        if (task.thumbnail == null) {
+            getThumbnailInBackground(task.key, lowResolution) { t: ThumbnailData? ->
+                task.thumbnail = t
+            }
+        }
+    }
+
+    /** Synchronously updates the thumbnail in the cache if it is already there. */
+    fun updateTaskSnapShot(taskId: Int, thumbnail: ThumbnailData?) {
+        Preconditions.assertUIThread()
+        cache.updateIfAlreadyInCache(taskId, thumbnail)
+    }
+
+    /**
+     * Asynchronously fetches the thumbnail for the given `task`.
+     *
+     * @param callback The callback to receive the task after its data has been populated.
+     *
+     * @return a cancelable handle to the request
+     */
+    override fun getThumbnailInBackground(
+        task: Task,
+        callback: Consumer<ThumbnailData>,
+    ): CancellableTask<ThumbnailData>? {
+        Preconditions.assertUIThread()
+
+        val lowResolution = !highResLoadingState.isEnabled
+        val taskThumbnail = task.thumbnail
+        if (
+            taskThumbnail?.thumbnail != null && (!taskThumbnail.reducedResolution || lowResolution)
+        ) {
+            // Nothing to load, the thumbnail is already high-resolution or matches what the
+            // request, so just callback
+            callback.accept(taskThumbnail)
+            return null
+        }
+
+        return getThumbnailInBackground(task.key, !highResLoadingState.isEnabled, callback)
+    }
+
+    /**
+     * Updates cache size and remove excess entries if current size is more than new cache size.
+     *
+     * @return whether cache size has increased
+     */
+    fun updateCacheSizeAndRemoveExcess(): Boolean {
+        val newSize = context.resources.getInteger(R.integer.recentsThumbnailCacheSize)
+        val oldSize = cache.maxSize
+        if (newSize == oldSize) {
+            // Return if no change in size
+            return false
+        }
+
+        cache.updateCacheSizeAndRemoveExcess(newSize)
+        return newSize > oldSize
+    }
+
+    private fun getThumbnailInBackground(
+        key: TaskKey,
+        lowResolution: Boolean,
+        callback: Consumer<ThumbnailData>,
+    ): CancellableTask<ThumbnailData>? {
+        Preconditions.assertUIThread()
+
+        val cachedThumbnail = cache.getAndInvalidateIfModified(key)
+        if (
+            cachedThumbnail?.thumbnail != null &&
+                (!cachedThumbnail.reducedResolution || lowResolution)
+        ) {
+            // Already cached, lets use that thumbnail
+            callback.accept(cachedThumbnail)
+            return null
+        }
+
+        val request =
+            CancellableTask(
+                {
+                    val thumbnailData =
+                        ActivityManagerWrapper.getInstance().getTaskThumbnail(key.id, lowResolution)
+                    if (thumbnailData.thumbnail != null) thumbnailData
+                    else ActivityManagerWrapper.getInstance().takeTaskThumbnail(key.id)
+                },
+                Executors.MAIN_EXECUTOR,
+                Consumer { result: ThumbnailData ->
+                    // Avoid an async timing issue that a low res entry replaces an existing high
+                    // res entry in high res enabled state, so we check before putting it to cache
+                    if (
+                        enableGridOnlyOverview() &&
+                            result.reducedResolution &&
+                            highResLoadingState.isEnabled
+                    ) {
+                        val newCachedThumbnail = cache.getAndInvalidateIfModified(key)
+                        if (
+                            newCachedThumbnail?.thumbnail != null &&
+                                !newCachedThumbnail.reducedResolution
+                        ) {
+                            return@Consumer
+                        }
+                    }
+                    cache.put(key, result)
+                    callback.accept(result)
+                },
+            )
+        bgExecutor.execute(request)
+        return request
+    }
+
+    /** Clears the cache. */
+    fun clear() {
+        cache.evictAll()
+    }
+
+    /** Removes the cached thumbnail for the given task. */
+    fun remove(key: TaskKey) {
+        cache.remove(key)
+    }
+
+    /** Returns The cache size. */
+    fun getCacheSize() = cache.maxSize
+
+    /** Returns Whether to enable background preloading of task thumbnails. */
+    fun isPreloadingEnabled() = enableTaskSnapshotPreloading && highResLoadingState.visible
+}
diff --git a/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt
index df546ca..ad2bd25 100644
--- a/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/HighResLoadingStateNotifier.kt
@@ -16,7 +16,7 @@
 
 package com.android.quickstep.recents.data
 
-import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback
 
 /** Notifies added callbacks that high res state has changed */
 interface HighResLoadingStateNotifier {
diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
index a45d194..608fafd 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
@@ -17,7 +17,7 @@
 package com.android.quickstep.recents.data
 
 import android.os.UserHandle
-import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback
 import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
 import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
 import com.android.quickstep.util.TaskVisualsChangeListener
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index 8a1b211..f950f47 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -207,7 +207,7 @@
                 val cancellableTask =
                     taskIconDataSource.getIconInBackground(task) { icon, contentDescription, title
                         ->
-                        icon.constantState?.let {
+                        icon?.constantState?.let {
                             continuation.resume(
                                 IconData(it.newDrawable().mutate(), contentDescription, title)
                             )
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 4660c51..cdf4efe 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -182,6 +182,7 @@
 import com.android.launcher3.util.coroutines.DispatcherProvider;
 import com.android.quickstep.BaseContainerInterface;
 import com.android.quickstep.GestureState;
+import com.android.quickstep.HighResLoadingState;
 import com.android.quickstep.OverviewCommandHelper;
 import com.android.quickstep.RecentsAnimationController;
 import com.android.quickstep.RecentsAnimationTargets;
@@ -194,7 +195,6 @@
 import com.android.quickstep.SplitSelectionListener;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskOverlayFactory;
-import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskViewUtils;
 import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.ViewUtils;
@@ -263,7 +263,7 @@
 public abstract class RecentsView<
         CONTAINER_TYPE extends Context & RecentsViewContainer & StatefulContainer<STATE_TYPE>,
         STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
-        TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
+        HighResLoadingState.HighResLoadingStateChangedCallback,
         TaskVisualsChangeListener {
 
     private static final String TAG = "RecentsView";
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt
index 7d09efd..4adf01e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeHighResLoadingStateNotifier.kt
@@ -16,7 +16,7 @@
 
 package com.android.quickstep.recents.data
 
-import com.android.quickstep.TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback
+import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback
 
 class FakeHighResLoadingStateNotifier : HighResLoadingStateNotifier {
     val listeners = mutableListOf<HighResLoadingStateChangedCallback>()
diff --git a/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java b/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
index 648fa93..ef4591e 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
+++ b/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
@@ -44,7 +44,6 @@
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -67,7 +66,7 @@
     private RecentTasksList mTasksList;
 
     @Mock
-    private TaskThumbnailCache.HighResLoadingState mHighResLoadingState;
+    private HighResLoadingState mHighResLoadingState;
 
     private RecentsModel mRecentsModel;