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;