Merge "Fixed delay of thumbnail loading when going to overview from home" into main
diff --git a/quickstep/res/values-sw600dp/config.xml b/quickstep/res/values-sw600dp/config.xml
new file mode 100644
index 0000000..b22cfc5
--- /dev/null
+++ b/quickstep/res/values-sw600dp/config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+<resources>
+ <!-- The number of thumbnails and icons to keep in the cache. The thumbnail cache size also
+ determines how many thumbnails will be fetched in the background. -->
+ <integer name="recentsThumbnailCacheSize">8</integer>
+</resources>
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 0a7344a..6ee2cfd 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -353,7 +353,8 @@
writer.println(prefix + " ]");
}
- private static class TaskLoadResult extends ArrayList<GroupTask> {
+ @VisibleForTesting
+ static class TaskLoadResult extends ArrayList<GroupTask> {
final int mRequestId;
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index d798e62..36a6eb6 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -17,15 +17,18 @@
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+import static com.android.launcher3.config.FeatureFlags.enableGridOnlyOverview;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.app.KeyguardManager;
+import android.content.ComponentCallbacks;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.Intent;
+import android.content.res.Configuration;
import android.os.Build;
import android.os.Process;
import android.os.UserHandle;
@@ -36,6 +39,7 @@
import com.android.launcher3.icons.IconProvider.IconChangeListener;
import com.android.launcher3.util.Executors.SimpleThreadFactory;
import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.SafeCloseable;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.TaskVisualsChangeListener;
import com.android.systemui.shared.recents.model.Task;
@@ -57,7 +61,7 @@
*/
@TargetApi(Build.VERSION_CODES.O)
public class RecentsModel implements IconChangeListener, TaskStackChangeListener,
- TaskVisualsChangeListener {
+ TaskVisualsChangeListener, SafeCloseable {
// We do not need any synchronization for this variable as its only written on UI thread.
public static final MainThreadInitializedObject<RecentsModel> INSTANCE =
@@ -72,17 +76,46 @@
private final RecentTasksList mTaskList;
private final TaskIconCache mIconCache;
private final TaskThumbnailCache mThumbnailCache;
+ private final ComponentCallbacks mCallbacks;
private RecentsModel(Context context) {
- mContext = context;
- mTaskList = new RecentTasksList(MAIN_EXECUTOR,
- context.getSystemService(KeyguardManager.class),
- SystemUiProxy.INSTANCE.get(context));
+ this(context, new IconProvider(context));
+ }
- IconProvider iconProvider = new IconProvider(context);
- mIconCache = new TaskIconCache(context, RECENTS_MODEL_EXECUTOR, iconProvider);
+ private RecentsModel(Context context, IconProvider iconProvider) {
+ this(context,
+ new RecentTasksList(MAIN_EXECUTOR,
+ context.getSystemService(KeyguardManager.class),
+ SystemUiProxy.INSTANCE.get(context)),
+ new TaskIconCache(context, RECENTS_MODEL_EXECUTOR, iconProvider),
+ new TaskThumbnailCache(context, RECENTS_MODEL_EXECUTOR),
+ iconProvider);
+ }
+
+ @VisibleForTesting
+ RecentsModel(Context context, RecentTasksList taskList, TaskIconCache iconCache,
+ TaskThumbnailCache thumbnailCache,
+ IconProvider iconProvider) {
+ mContext = context;
+ mTaskList = taskList;
+ mIconCache = iconCache;
mIconCache.registerTaskVisualsChangeListener(this);
- mThumbnailCache = new TaskThumbnailCache(context, RECENTS_MODEL_EXECUTOR);
+ mThumbnailCache = thumbnailCache;
+ if (enableGridOnlyOverview()) {
+ mCallbacks = new ComponentCallbacks() {
+ @Override
+ public void onConfigurationChanged(Configuration configuration) {
+ updateCacheSizeAndPreloadIfNeeded();
+ }
+
+ @Override
+ public void onLowMemory() {
+ }
+ };
+ context.registerComponentCallbacks(mCallbacks);
+ } else {
+ mCallbacks = null;
+ }
TaskStackChangeListeners.getInstance().registerTaskStackListener(this);
iconProvider.registerIconChangeListener(this, MAIN_EXECUTOR.getHandler());
@@ -109,7 +142,6 @@
RecentsFilterState.DEFAULT_FILTER);
}
-
/**
* Fetches the list of recent tasks, based on a filter
*
@@ -183,8 +215,8 @@
// time the user next enters overview
continue;
}
- mThumbnailCache.updateThumbnailInCache(group.task1);
- mThumbnailCache.updateThumbnailInCache(group.task2);
+ mThumbnailCache.updateThumbnailInCache(group.task1, /* lowResolution= */ true);
+ mThumbnailCache.updateThumbnailInCache(group.task2, /* lowResolution= */ true);
}
});
}
@@ -281,6 +313,54 @@
}
/**
+ * Preloads cache if enableGridOnlyOverview is true, preloading is enabled and
+ * highResLoadingState is enabled
+ */
+ public void preloadCacheIfNeeded() {
+ if (!enableGridOnlyOverview()) {
+ return;
+ }
+
+ if (!mThumbnailCache.isPreloadingEnabled()) {
+ // Skip if we aren't preloading.
+ return;
+ }
+
+ if (!mThumbnailCache.getHighResLoadingState().isEnabled()) {
+ // Skip if high-res loading state is disabled.
+ return;
+ }
+
+ mTaskList.getTaskKeys(mThumbnailCache.getCacheSize(), taskGroups -> {
+ for (GroupTask group : taskGroups) {
+ mThumbnailCache.updateThumbnailInCache(group.task1, /* lowResolution= */ false);
+ mThumbnailCache.updateThumbnailInCache(group.task2, /* lowResolution= */ false);
+ }
+ });
+ }
+
+ /**
+ * Updates cache size and preloads more tasks if cache size increases
+ */
+ public void updateCacheSizeAndPreloadIfNeeded() {
+ if (!enableGridOnlyOverview()) {
+ return;
+ }
+
+ // If new size is larger than original size, preload more cache to fill the gap
+ if (mThumbnailCache.updateCacheSizeAndRemoveExcess()) {
+ preloadCacheIfNeeded();
+ }
+ }
+
+ @Override
+ public void close() {
+ if (mCallbacks != null) {
+ mContext.unregisterComponentCallbacks(mCallbacks);
+ }
+ }
+
+ /**
* Listener for receiving running tasks changes
*/
public interface RunningTasksListener {
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index 3175ba8..2ca9f99 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -15,12 +15,18 @@
*/
package com.android.quickstep;
+import static com.android.launcher3.config.FeatureFlags.enableGridOnlyOverview;
+
import android.content.Context;
import android.content.res.Resources;
+import androidx.annotation.VisibleForTesting;
+
import com.android.launcher3.R;
import com.android.launcher3.util.Preconditions;
import com.android.quickstep.util.CancellableTask;
+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;
@@ -34,11 +40,10 @@
public class TaskThumbnailCache {
private final Executor mBgExecutor;
-
- private final int mCacheSize;
- private final TaskKeyLruCache<ThumbnailData> mCache;
+ private final TaskKeyCache<ThumbnailData> mCache;
private final HighResLoadingState mHighResLoadingState;
private final boolean mEnableTaskSnapshotPreloading;
+ private final Context mContext;
public static class HighResLoadingState {
private boolean mForceHighResThumbnails;
@@ -91,26 +96,39 @@
}
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();
- mCacheSize = res.getInteger(R.integer.recentsThumbnailCacheSize);
mEnableTaskSnapshotPreloading = res.getBoolean(R.bool.config_enableTaskSnapshotPreloading);
- mCache = new TaskKeyLruCache<>(mCacheSize);
+ mCache = cache;
}
/**
- * Synchronously fetches the thumbnail for the given {@param task} and puts it in the 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) {
+ 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) {
- updateThumbnailInBackground(task.key, true /* lowResolution */,
+ updateThumbnailInBackground(task.key, lowResolution,
t -> task.thumbnail = t);
}
}
@@ -148,6 +166,23 @@
});
}
+ /**
+ * 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 updateThumbnailInBackground(TaskKey key, boolean lowResolution,
Consumer<ThumbnailData> callback) {
Preconditions.assertUIThread();
@@ -169,6 +204,16 @@
@Override
public void handleResult(ThumbnailData 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 cachedThumbnail = mCache.getAndInvalidateIfModified(key);
+ if (cachedThumbnail != null && cachedThumbnail.thumbnail != null
+ && !cachedThumbnail.reducedResolution) {
+ return;
+ }
+ }
mCache.put(key, result);
callback.accept(result);
}
@@ -195,7 +240,7 @@
* @return The cache size.
*/
public int getCacheSize() {
- return mCacheSize;
+ return mCache.getMaxSize();
}
/**
diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java
new file mode 100644
index 0000000..79ca076
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2023 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.util;
+
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.systemui.shared.recents.model.Task;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
+
+/**
+ * A class to cache task id and its corresponding object (e.g. thumbnail)
+ *
+ * <p>Maximum size of the cache should be provided when creating this class. When the number of
+ * entries is larger than its max size, it would remove the entry with the smallest last active time
+ * @param <V> Type of object stored in the cache
+ */
+public class TaskKeyByLastActiveTimeCache<V> implements TaskKeyCache<V> {
+ private static final String TAG = TaskKeyByLastActiveTimeCache.class.getSimpleName();
+ private final AtomicInteger mMaxSize;
+ private final Map<Integer, Entry<V>> mMap;
+ // To sort task id by last active time
+ private final PriorityQueue<Task.TaskKey> mQueue;
+
+ public TaskKeyByLastActiveTimeCache(int maxSize) {
+ mMap = new HashMap(maxSize);
+ mQueue = new PriorityQueue<>(Comparator.comparingLong(t -> t.lastActiveTime));
+ mMaxSize = new AtomicInteger(maxSize);
+ }
+
+ /**
+ * Removes all entries from the cache
+ */
+ @Override
+ public synchronized void evictAll() {
+ mMap.clear();
+ mQueue.clear();
+ }
+
+
+ /**
+ * Removes a particular entry from the cache
+ */
+ @Override
+ public synchronized void remove(Task.TaskKey key) {
+ if (key == null) {
+ return;
+ }
+
+ Entry<V> entry = mMap.remove(key.id);
+ if (entry != null) {
+ // Use real key in map entry to handle use case of using stub key for removal
+ mQueue.remove(entry.mKey);
+ }
+ }
+
+ /**
+ * Removes all entries matching keyCheck
+ */
+ @Override
+ public synchronized void removeAll(Predicate<Task.TaskKey> keyCheck) {
+ Iterator<Task.TaskKey> iterator = mQueue.iterator();
+ while (iterator.hasNext()) {
+ Task.TaskKey key = iterator.next();
+ if (keyCheck.test(key)) {
+ mMap.remove(key.id);
+ iterator.remove();
+ }
+ }
+ }
+
+ /**
+ * Gets the entry if it is still valid
+ */
+ @Override
+ public synchronized V getAndInvalidateIfModified(Task.TaskKey key) {
+ Entry<V> entry = mMap.get(key.id);
+ if (entry != null && entry.mKey.windowingMode == key.windowingMode
+ && entry.mKey.lastActiveTime == key.lastActiveTime) {
+ return entry.mValue;
+ } else {
+ remove(key);
+ return null;
+ }
+ }
+
+ /**
+ * Adds an entry to the cache, optionally evicting the last accessed entry
+ */
+ @Override
+ public final synchronized void put(Task.TaskKey key, V value) {
+ if (key != null && value != null) {
+ Entry<V> entry = mMap.get(key.id);
+ // If the same key already exist, remove item for existing key
+ if (entry != null) {
+ mQueue.remove(entry.mKey);
+ }
+
+ mMap.put(key.id, new Entry<>(key, value));
+ mQueue.add(key);
+ removeExcessIfNeeded();
+ } else {
+ Log.e(TAG, "Unexpected null key or value: " + key + ", " + value);
+ }
+ }
+
+ /**
+ * Updates the cache entry if it is already present in the cache
+ */
+ @Override
+ public synchronized void updateIfAlreadyInCache(int taskId, V data) {
+ Entry<V> entry = mMap.get(taskId);
+ if (entry != null) {
+ entry.mValue = data;
+ }
+ }
+
+ /**
+ * Updates cache size and remove excess if the number of existing entries is larger than new
+ * cache size
+ */
+ @Override
+ public synchronized void updateCacheSizeAndRemoveExcess(int cacheSize) {
+ mMaxSize.compareAndSet(mMaxSize.get(), cacheSize);
+ removeExcessIfNeeded();
+ }
+
+ private synchronized void removeExcessIfNeeded() {
+ while (mQueue.size() > mMaxSize.get() && !mQueue.isEmpty()) {
+ Task.TaskKey key = mQueue.poll();
+ mMap.remove(key.id);
+ }
+ }
+
+ /**
+ * Get maximum size of the cache
+ */
+ @Override
+ public int getMaxSize() {
+ return mMaxSize.get();
+ }
+
+ /**
+ * Get current size of the cache
+ */
+ @Override
+ public int getSize() {
+ return mMap.size();
+ }
+
+ @VisibleForTesting
+ PriorityQueue<Task.TaskKey> getQueue() {
+ return mQueue;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyCache.java
new file mode 100644
index 0000000..8ee78ab
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskKeyCache.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 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.util;
+
+import com.android.systemui.shared.recents.model.Task;
+
+import java.util.function.Predicate;
+
+/**
+ * An interface for caching task id and its corresponding object (e.g. thumbnail, task icon)
+ *
+ * @param <V> Type of object stored in the cache
+ */
+public interface TaskKeyCache<V> {
+
+ /**
+ * Removes all entries from the cache.
+ */
+ void evictAll();
+
+ /**
+ * Removes a particular entry from the cache.
+ */
+ void remove(Task.TaskKey key);
+
+ /**
+ * Removes all entries matching keyCheck.
+ */
+ void removeAll(Predicate<Task.TaskKey> keyCheck);
+
+ /**
+ * Gets the entry if it is still valid.
+ */
+ V getAndInvalidateIfModified(Task.TaskKey key);
+
+ /**
+ * Adds an entry to the cache, optionally evicting the last accessed entry.
+ */
+ void put(Task.TaskKey key, V value);
+
+ /**
+ * Updates the cache entry if it is already present in the cache.
+ */
+ void updateIfAlreadyInCache(int taskId, V data);
+
+ /**
+ * Updates cache size and remove excess if the number of existing entries is larger than new
+ * cache size.
+ */
+ default void updateCacheSizeAndRemoveExcess(int cacheSize) { }
+
+ /**
+ * Gets maximum size of the cache.
+ */
+ int getMaxSize();
+
+ /**
+ * Gets current size of the cache.
+ */
+ int getSize();
+
+ class Entry<V> {
+
+ final Task.TaskKey mKey;
+ V mValue;
+
+ Entry(Task.TaskKey key, V value) {
+ mKey = key;
+ mValue = value;
+ }
+
+ @Override
+ public int hashCode() {
+ return mKey.id;
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java
index 08a65fa..89f5d41 100644
--- a/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java
+++ b/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java
@@ -27,7 +27,7 @@
* A simple LRU cache for task key entries
* @param <V> The type of the value
*/
-public class TaskKeyLruCache<V> {
+public class TaskKeyLruCache<V> implements TaskKeyCache<V> {
private final MyLinkedHashMap<V> mMap;
@@ -92,20 +92,14 @@
}
}
- private static class Entry<V> {
+ @Override
+ public int getMaxSize() {
+ return mMap.mMaxSize;
+ }
- final TaskKey mKey;
- V mValue;
-
- Entry(TaskKey key, V value) {
- mKey = key;
- mValue = value;
- }
-
- @Override
- public int hashCode() {
- return mKey.id;
- }
+ @Override
+ public int getSize() {
+ return mMap.size();
}
private static class MyLinkedHashMap<V> extends LinkedHashMap<Integer, Entry<V>> {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 8888c0d..825c0ae 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -2277,10 +2277,12 @@
if (showAsGrid()) {
int screenStart = mOrientationHandler.getPrimaryScroll(this);
int pageOrientedSize = mOrientationHandler.getMeasuredSize(this);
- int halfScreenSize = pageOrientedSize / 2;
- // Use +/- 50% screen width as visible area.
- visibleStart = screenStart - halfScreenSize;
- visibleEnd = screenStart + pageOrientedSize + halfScreenSize;
+ // For GRID_ONLY_OVERVIEW, use +/- 1 task column as visible area for preloading
+ // adjacent thumbnails, otherwise use +/-50% screen width
+ int extraWidth = enableGridOnlyOverview() ? getLastComputedTaskSize().width()
+ + getPageSpacing() : pageOrientedSize / 2;
+ visibleStart = screenStart - extraWidth;
+ visibleEnd = screenStart + pageOrientedSize + extraWidth;
} else {
int centerPageIndex = getPageNearestToCenterOfScreen();
int numChildren = getChildCount();
@@ -2361,6 +2363,12 @@
@Override
public void onHighResLoadingStateChanged(boolean enabled) {
+ // Preload cache when no overview task is visible (e.g. not in overview page), so when
+ // user goes to overview next time, the task thumbnails would show up without delay
+ if (mHasVisibleTaskData.size() == 0) {
+ mModel.preloadCacheIfNeeded();
+ }
+
// Whenever the high res loading state changes, poke each of the visible tasks to see if
// they want to updated their thumbnail state
for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
diff --git a/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java b/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
new file mode 100644
index 0000000..08e0898
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2023 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.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.Flags;
+import com.android.launcher3.R;
+import com.android.launcher3.icons.IconProvider;
+import com.android.quickstep.util.GroupTask;
+import com.android.systemui.shared.recents.model.Task;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+@SmallTest
+public class RecentsModelTest {
+ @Mock
+ private Context mContext;
+
+ @Mock
+ private TaskThumbnailCache mThumbnailCache;
+
+ @Mock
+ private RecentTasksList mTasksList;
+
+ @Mock
+ private TaskThumbnailCache.HighResLoadingState mHighResLoadingState;
+
+ private RecentsModel mRecentsModel;
+
+ private RecentTasksList.TaskLoadResult mTaskResult;
+
+ private Resources mResource;
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ @Before
+ public void setup() throws NoSuchFieldException {
+ MockitoAnnotations.initMocks(this);
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW);
+ mTaskResult = getTaskResult();
+ doAnswer(invocation-> {
+ Consumer<ArrayList<GroupTask>> callback = invocation.getArgument(1);
+ callback.accept(mTaskResult);
+ return null;
+ }).when(mTasksList).getTaskKeys(anyInt(), any());
+
+ when(mHighResLoadingState.isEnabled()).thenReturn(true);
+ when(mThumbnailCache.getHighResLoadingState()).thenReturn(mHighResLoadingState);
+ when(mThumbnailCache.isPreloadingEnabled()).thenReturn(true);
+
+ mRecentsModel = new RecentsModel(mContext, mTasksList, mock(TaskIconCache.class),
+ mThumbnailCache, mock(IconProvider.class));
+
+ mResource = mock(Resources.class);
+ when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(3);
+ when(mContext.getResources()).thenReturn(mResource);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW);
+ }
+
+ @Test
+ @UiThreadTest
+ public void preloadOnHighResolutionEnabled() {
+ mRecentsModel.preloadCacheIfNeeded();
+
+ ArgumentCaptor<Task> taskArgs = ArgumentCaptor.forClass(Task.class);
+ verify(mRecentsModel.getThumbnailCache(), times(2))
+ .updateThumbnailInCache(taskArgs.capture(), /* lowResolution= */ eq(false));
+
+ GroupTask expectedGroupTask = mTaskResult.get(0);
+ assertThat(taskArgs.getAllValues().get(0)).isEqualTo(
+ expectedGroupTask.task1);
+ assertThat(taskArgs.getAllValues().get(1)).isEqualTo(
+ expectedGroupTask.task2);
+ }
+
+ @Test
+ public void notPreloadOnHighResolutionDisabled() {
+ when(mHighResLoadingState.isEnabled()).thenReturn(false);
+ when(mThumbnailCache.isPreloadingEnabled()).thenReturn(true);
+ mRecentsModel.preloadCacheIfNeeded();
+ verify(mRecentsModel.getThumbnailCache(), never())
+ .updateThumbnailInCache(any(), anyBoolean());
+ }
+
+ @Test
+ public void notPreloadOnPreloadDisabled() {
+ when(mThumbnailCache.isPreloadingEnabled()).thenReturn(false);
+ mRecentsModel.preloadCacheIfNeeded();
+ verify(mRecentsModel.getThumbnailCache(), never())
+ .updateThumbnailInCache(any(), anyBoolean());
+
+ }
+
+ @Test
+ public void increaseCacheSizeAndPreload() {
+ // Mock to return preload is needed
+ when(mThumbnailCache.updateCacheSizeAndRemoveExcess()).thenReturn(true);
+ // Update cache size
+ mRecentsModel.updateCacheSizeAndPreloadIfNeeded();
+ // Assert update cache is called
+ verify(mRecentsModel.getThumbnailCache(), times(2))
+ .updateThumbnailInCache(any(), /* lowResolution= */ eq(false));
+ }
+
+ @Test
+ public void decreaseCacheSizeAndNotPreload() {
+ // Mock to return preload is not needed
+ when(mThumbnailCache.updateCacheSizeAndRemoveExcess()).thenReturn(false);
+ // Update cache size
+ mRecentsModel.updateCacheSizeAndPreloadIfNeeded();
+ // Assert update cache is never called
+ verify(mRecentsModel.getThumbnailCache(), never())
+ .updateThumbnailInCache(any(), anyBoolean());
+ }
+
+ private RecentTasksList.TaskLoadResult getTaskResult() {
+ RecentTasksList.TaskLoadResult allTasks = new RecentTasksList.TaskLoadResult(0, false, 1);
+ ActivityManager.RecentTaskInfo taskInfo1 = new ActivityManager.RecentTaskInfo();
+ Task.TaskKey taskKey1 = new Task.TaskKey(taskInfo1);
+ Task task1 = Task.from(taskKey1, taskInfo1, false);
+
+ ActivityManager.RecentTaskInfo taskInfo2 = new ActivityManager.RecentTaskInfo();
+ Task.TaskKey taskKey2 = new Task.TaskKey(taskInfo2);
+ Task task2 = Task.from(taskKey2, taskInfo2, false);
+
+ allTasks.add(new GroupTask(task1, task2, null));
+ return allTasks;
+ }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaskThumbnailCacheTest.java b/quickstep/tests/src/com/android/quickstep/TaskThumbnailCacheTest.java
new file mode 100644
index 0000000..4e04261
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/TaskThumbnailCacheTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2023 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 junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.R;
+import com.android.quickstep.util.TaskKeyCache;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.Executor;
+
+@SmallTest
+public class TaskThumbnailCacheTest {
+ @Mock
+ private Context mContext;
+
+ @Mock
+ private Resources mResource;
+
+ @Mock
+ private TaskKeyCache mTaskKeyCache;
+
+ @Before
+ public void setup() throws NoSuchFieldException {
+ MockitoAnnotations.initMocks(this);
+ when(mContext.getResources()).thenReturn(mResource);
+ }
+
+ @Test
+ public void increaseCacheSize() {
+ // Mock a cache size increase from 3 to 8
+ when(mTaskKeyCache.getMaxSize()).thenReturn(3);
+ when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(8);
+ TaskThumbnailCache thumbnailCache = new TaskThumbnailCache(mContext, mock(Executor.class),
+ mTaskKeyCache);
+
+ // Preload is needed when increasing size
+ assertTrue(thumbnailCache.updateCacheSizeAndRemoveExcess());
+ verify(mTaskKeyCache, times(1)).updateCacheSizeAndRemoveExcess(8);
+ }
+
+ @Test
+ public void decreaseCacheSize() {
+ // Mock a cache size decrease from 8 to 3
+ when(mTaskKeyCache.getMaxSize()).thenReturn(8);
+ when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(3);
+ TaskThumbnailCache thumbnailCache = new TaskThumbnailCache(mContext, mock(Executor.class),
+ mTaskKeyCache);
+ // Preload is not needed when decreasing size
+ assertFalse(thumbnailCache.updateCacheSizeAndRemoveExcess());
+ verify(mTaskKeyCache, times(1)).updateCacheSizeAndRemoveExcess(3);
+ }
+
+ @Test
+ public void keepSameCacheSize() {
+ when(mTaskKeyCache.getMaxSize()).thenReturn(3);
+ when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(3);
+ TaskThumbnailCache thumbnailCache = new TaskThumbnailCache(mContext, mock(Executor.class),
+ mTaskKeyCache);
+ // Preload is not needed when it has the same cache size
+ assertFalse(thumbnailCache.updateCacheSizeAndRemoveExcess());
+ verify(mTaskKeyCache, never()).updateCacheSizeAndRemoveExcess(anyInt());
+ }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCacheTest.java b/quickstep/tests/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCacheTest.java
new file mode 100644
index 0000000..ea2688a
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCacheTest.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2023 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.util;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+
+import android.content.ComponentName;
+import android.content.Intent;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+import org.junit.Test;
+
+@SmallTest
+public class TaskKeyByLastActiveTimeCacheTest {
+ @Test
+ public void add() {
+ TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+ Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+ new ComponentName("", ""), 0, 1);
+ ThumbnailData data1 = new ThumbnailData();
+ cache.put(key1, data1);
+
+ Task.TaskKey key2 = new Task.TaskKey(2, 0, new Intent(),
+ new ComponentName("", ""), 0, 2);
+ ThumbnailData data2 = new ThumbnailData();
+ cache.put(key2, data2);
+
+ assertEquals(2, cache.getSize());
+ assertEquals(data1, cache.getAndInvalidateIfModified(key1));
+ assertEquals(data2, cache.getAndInvalidateIfModified(key2));
+
+ assertEquals(2, cache.getQueue().size());
+ assertEquals(key1, cache.getQueue().poll());
+ assertEquals(key2, cache.getQueue().poll());
+ }
+
+ @Test
+ public void addSameTasksWithSameLastActiveTimeTwice() {
+ // Add 2 tasks with same id and last active time, it should only have 1 entry in cache
+ TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+ Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+ new ComponentName("", ""), 0, 1000);
+ ThumbnailData data1 = new ThumbnailData();
+ cache.put(key1, data1);
+
+ Task.TaskKey key2 = new Task.TaskKey(1, 0, new Intent(),
+ new ComponentName("", ""), 0, 1000);
+ ThumbnailData data2 = new ThumbnailData();
+ cache.put(key2, data2);
+
+ assertEquals(1, cache.getSize());
+ assertEquals(data2, cache.getAndInvalidateIfModified(key2));
+
+ assertEquals(1, cache.getQueue().size());
+ assertEquals(key2, cache.getQueue().poll());
+ }
+
+ @Test
+ public void addSameTasksWithDifferentLastActiveTime() {
+ // Add 2 tasks with same id and different last active time, it should only have the
+ // higher last active time entry
+ TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+ Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+ new ComponentName("", ""), 0, 1000);
+ ThumbnailData data1 = new ThumbnailData();
+ cache.put(key1, data1);
+
+ Task.TaskKey key2 = new Task.TaskKey(1, 0, new Intent(),
+ new ComponentName("", ""), 0, 2000);
+ ThumbnailData data2 = new ThumbnailData();
+ cache.put(key2, data2);
+
+ assertEquals(1, cache.getSize());
+ assertEquals(data2, cache.getAndInvalidateIfModified(key2));
+
+ assertEquals(1, cache.getQueue().size());
+ Task.TaskKey queueKey = cache.getQueue().poll();
+ assertEquals(key2, queueKey);
+ // TaskKey's equal method does not check last active time, so we check here
+ assertEquals(2000, queueKey.lastActiveTime);
+ }
+
+ @Test
+ public void remove() {
+ TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+ Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+ new ComponentName("", ""), 0, 0);
+ cache.put(key1, new ThumbnailData());
+
+ cache.remove(key1);
+
+ assertEquals(0, cache.getSize());
+ assertEquals(0, cache.getQueue().size());
+ }
+
+ @Test
+ public void removeByStubKey() {
+ TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+ Task.TaskKey key1 = new Task.TaskKey(1, 1, new Intent(),
+ new ComponentName("", ""), 1, 100);
+ cache.put(key1, new ThumbnailData());
+
+ Task.TaskKey stubKey = new Task.TaskKey(1, 0, new Intent(),
+ new ComponentName("", ""), 0, 0);
+ cache.remove(stubKey);
+
+ assertEquals(0, cache.getSize());
+ assertEquals(0, cache.getQueue().size());
+ }
+
+ @Test
+ public void evictAll() {
+ TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+ Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+ new ComponentName("", ""), 0, 0);
+ cache.put(key1, new ThumbnailData());
+ Task.TaskKey key2 = new Task.TaskKey(2, 0, new Intent(),
+ new ComponentName("", ""), 0, 0);
+ cache.put(key2, new ThumbnailData());
+
+ cache.evictAll();
+
+ assertEquals(0, cache.getSize());
+ assertEquals(0, cache.getQueue().size());
+ }
+
+ @Test
+ public void removeAllByPredicate() {
+ TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+ // Add user 1's tasks
+ Task.TaskKey user1Key1 = new Task.TaskKey(1, 0, new Intent(),
+ new ComponentName("", ""), 1, 0);
+ cache.put(user1Key1, new ThumbnailData());
+ Task.TaskKey user1Key2 = new Task.TaskKey(2, 0, new Intent(),
+ new ComponentName("", ""), 1, 0);
+ cache.put(user1Key2, new ThumbnailData());
+ // Add user 2's task
+ Task.TaskKey user2Key = new Task.TaskKey(3, 0, new Intent(),
+ new ComponentName("", ""), 2, 0);
+ ThumbnailData user2Data = new ThumbnailData();
+ cache.put(user2Key, user2Data);
+
+ cache.removeAll(key -> key.userId == 1);
+
+ // Only user 2's task remains
+ assertEquals(1, cache.getSize());
+ assertEquals(user2Data, cache.getAndInvalidateIfModified(user2Key));
+
+ assertEquals(1, cache.getQueue().size());
+ assertEquals(user2Key, cache.getQueue().poll());
+ }
+
+ @Test
+ public void getAndInvalidateIfModified() {
+ TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+ // Add user 1's tasks
+ Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+ new ComponentName("", ""), 1, 0);
+ ThumbnailData data1 = new ThumbnailData();
+ cache.put(key1, data1);
+
+ // Get result with task key of same last active time
+ Task.TaskKey keyWithSameActiveTime = new Task.TaskKey(1, 0, new Intent(),
+ new ComponentName("", ""), 1, 0);
+ ThumbnailData result1 = cache.getAndInvalidateIfModified(keyWithSameActiveTime);
+ assertEquals(data1, result1);
+ assertEquals(1, cache.getQueue().size());
+
+ // Invalidate result with task key of new last active time
+ Task.TaskKey keyWithNewActiveTime = new Task.TaskKey(1, 0, new Intent(),
+ new ComponentName("", ""), 1, 1);
+ ThumbnailData result2 = cache.getAndInvalidateIfModified(keyWithNewActiveTime);
+ // No entry is retrieved because the key has higher last active time
+ assertNull(result2);
+ assertEquals(0, cache.getSize());
+ assertEquals(0, cache.getQueue().size());
+ }
+
+ @Test
+ public void removeByLastActiveTimeWhenOverMaxSize() {
+ TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(2);
+ Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+ new ComponentName("", ""), 0, 200);
+ ThumbnailData task1 = new ThumbnailData();
+ cache.put(key1, task1);
+ Task.TaskKey key2 = new Task.TaskKey(2, 0, new Intent(),
+ new ComponentName("", ""), 0, 100);
+ ThumbnailData task2 = new ThumbnailData();
+ cache.put(key2, task2);
+
+ // Add the 3rd entry which will exceed the max cache size
+ Task.TaskKey key3 = new Task.TaskKey(3, 0, new Intent(),
+ new ComponentName("", ""), 0, 300);
+ ThumbnailData task3 = new ThumbnailData();
+ cache.put(key3, task3);
+
+ // Assert map size and check the remaining entries have higher active time
+ assertEquals(2, cache.getSize());
+ assertEquals(task1, cache.getAndInvalidateIfModified(key1));
+ assertEquals(task3, cache.getAndInvalidateIfModified(key3));
+ assertNull(cache.getAndInvalidateIfModified(key2));
+
+ // Assert queue size and check the remaining entries have higher active time
+ assertEquals(2, cache.getQueue().size());
+ Task.TaskKey queueKey1 = cache.getQueue().poll();
+ assertEquals(key1, queueKey1);
+ assertEquals(200, queueKey1.lastActiveTime);
+ Task.TaskKey queueKey2 = cache.getQueue().poll();
+ assertEquals(key3, queueKey2);
+ assertEquals(300, queueKey2.lastActiveTime);
+ }
+
+ @Test
+ public void updateIfAlreadyInCache() {
+ TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(2);
+ Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+ new ComponentName("", ""), 0, 200);
+ cache.put(key1, new ThumbnailData());
+
+ // Update original data to new data
+ ThumbnailData newData = new ThumbnailData();
+ cache.updateIfAlreadyInCache(key1.id, newData);
+
+ // Data is updated to newData successfully
+ ThumbnailData result = cache.getAndInvalidateIfModified(key1);
+ assertEquals(newData, result);
+ }
+
+ @Test
+ public void updateCacheSizeAndInvalidateExcess() {
+ // Last active time are not in-sync with insertion order to simulate the real async case
+ TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(4);
+ Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+ new ComponentName("", ""), 0, 200);
+ cache.put(key1, new ThumbnailData());
+
+ Task.TaskKey key2 = new Task.TaskKey(2, 0, new Intent(),
+ new ComponentName("", ""), 0, 100);
+ cache.put(key2, new ThumbnailData());
+
+ Task.TaskKey key3 = new Task.TaskKey(3, 0, new Intent(),
+ new ComponentName("", ""), 0, 400);
+ cache.put(key3, new ThumbnailData());
+
+ Task.TaskKey key4 = new Task.TaskKey(4, 0, new Intent(),
+ new ComponentName("", ""), 0, 300);
+ cache.put(key4, new ThumbnailData());
+
+ // Check that it has 4 entries before cache size changes
+ assertEquals(4, cache.getSize());
+ assertEquals(4, cache.getQueue().size());
+
+ // Update size to 2
+ cache.updateCacheSizeAndRemoveExcess(2);
+
+ // Number of entries becomes 2, only key3 and key4 remain
+ assertEquals(2, cache.getSize());
+ assertEquals(2, cache.getQueue().size());
+ assertNotNull(cache.getAndInvalidateIfModified(key3));
+ assertNotNull(cache.getAndInvalidateIfModified(key4));
+ }
+}