Merge "Caching clean up, remove dependency on old shared lib loading/caching logic" into ub-launcher3-master
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index c294376..d8ca1c4 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -19,4 +19,9 @@
     <string name="overview_callbacks_class" translatable="false"></string>
 
     <string name="user_event_dispatcher_class" translatable="false">com.android.quickstep.logging.UserEventDispatcherExtension</string>
+
+    <!-- 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">3</integer>
+    <integer name="recentsIconCacheSize">12</integer>
 </resources>
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
index b406b30..4e79fed 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -163,19 +163,12 @@
         }
     }
 
-    public static void onStart(Context context) {
-        RecentsModel model = RecentsModel.INSTANCE.get(context);
-        if (model != null) {
-            model.onStart();
-        }
-    }
-
     public static void onEnterAnimationComplete(Context context) {
         // After the transition to home, enable the high-res thumbnail loader if it wasn't enabled
         // as a part of quickstep/scrub, so that high-res thumbnails can load the next time we
         // enter overview
-        RecentsModel.INSTANCE.get(context).getRecentsTaskLoader()
-                .getHighResThumbnailLoader().setVisible(true);
+        RecentsModel.INSTANCE.get(context).getThumbnailCache()
+                .getHighResLoadingState().setVisible(true);
     }
 
     public static void onLauncherStateOrResumeChanged(Launcher launcher) {
diff --git a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
index 94ec69a..b11260e 100644
--- a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
+++ b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
@@ -232,7 +232,7 @@
                 mInputConsumer, mTouchInteractionLog);
 
         // Preload the plan
-        mRecentsModel.loadTasks(mRunningTask.id, null);
+        mRecentsModel.getTasks(null);
         mInteractionHandler = handler;
         handler.setGestureEndCallback(mEventQueue::reset);
 
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index d9626c4..5b488ca 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -230,7 +230,7 @@
             mRunningTaskId = mAM.getRunningTask().id;
 
             // Preload the plan
-            mRecentsModel.loadTasks(mRunningTaskId, null);
+            mRecentsModel.getTasks(null);
         }
 
         @Override
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
new file mode 100644
index 0000000..fec38bf
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2014 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.os.Process;
+import android.util.SparseBooleanArray;
+import com.android.launcher3.MainThreadExecutor;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.BackgroundExecutor;
+import com.android.systemui.shared.system.KeyguardManagerCompat;
+import com.android.systemui.shared.system.RecentTaskInfoCompat;
+import com.android.systemui.shared.system.TaskDescriptionCompat;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Manages the recent task list from the system, caching it as necessary.
+ */
+public class RecentTasksList extends TaskStackChangeListener {
+
+    private final KeyguardManagerCompat mKeyguardManager;
+    private final MainThreadExecutor mMainThreadExecutor;
+    private final BackgroundExecutor mBgThreadExecutor;
+
+    // The list change id, increments as the task list changes in the system
+    private int mChangeId;
+    // The last change id when the list was last loaded completely, must be <= the list change id
+    private int mLastLoadedId;
+
+    ArrayList<Task> mTasks = new ArrayList<>();
+
+    public RecentTasksList(Context context) {
+        mMainThreadExecutor = new MainThreadExecutor();
+        mBgThreadExecutor = BackgroundExecutor.get();
+        mKeyguardManager = new KeyguardManagerCompat(context);
+        mChangeId = 1;
+    }
+
+    /**
+     * Asynchronously fetches the list of recent tasks.
+     *
+     * @param numTasks The maximum number of tasks to fetch
+     * @param loadKeysOnly Whether to load other associated task data, or just the key
+     * @param callback The callback to receive the list of recent tasks
+     * @return The change id of the current task list
+     */
+    public synchronized int getTasks(int numTasks, boolean loadKeysOnly,
+            Consumer<ArrayList<Task>> callback) {
+        final int requestLoadId = mChangeId;
+        final int numLoadTasks = numTasks > 0
+                ? numTasks
+                : Integer.MAX_VALUE;
+
+        if (mLastLoadedId == mChangeId) {
+            // The list is up to date, callback with the same list
+            mMainThreadExecutor.execute(() -> {
+                if (callback != null) {
+                    callback.accept(mTasks);
+                }
+            });
+        }
+
+        // Kick off task loading in the background
+        mBgThreadExecutor.submit(() -> {
+            ArrayList<Task> tasks = loadTasksInBackground(numLoadTasks,
+                    loadKeysOnly);
+
+            mMainThreadExecutor.execute(() -> {
+                mTasks = tasks;
+                mLastLoadedId = requestLoadId;
+
+                if (callback != null) {
+                    callback.accept(tasks);
+                }
+            });
+        });
+
+        return requestLoadId;
+    }
+
+    /**
+     * @return Whether the provided {@param changeId} is the latest recent tasks list id.
+     */
+    public synchronized boolean isTaskListValid(int changeId) {
+        return mChangeId == changeId;
+    }
+
+    @Override
+    public synchronized void onTaskStackChanged() {
+        mChangeId++;
+    }
+
+    @Override
+    public synchronized void onActivityPinned(String packageName, int userId, int taskId,
+            int stackId) {
+        mChangeId++;
+    }
+
+    @Override
+    public synchronized void onActivityUnpinned() {
+        mChangeId++;
+    }
+
+    /**
+     * Loads and creates a list of all the recent tasks.
+     */
+    private ArrayList<Task> loadTasksInBackground(int numTasks,
+            boolean loadKeysOnly) {
+        int currentUserId = Process.myUserHandle().getIdentifier();
+        ArrayList<Task> allTasks = new ArrayList<>();
+        List<ActivityManager.RecentTaskInfo> rawTasks =
+                ActivityManagerWrapper.getInstance().getRecentTasks(numTasks, currentUserId);
+        // The raw tasks are given in most-recent to least-recent order, we need to reverse it
+        Collections.reverse(rawTasks);
+
+        SparseBooleanArray tmpLockedUsers = new SparseBooleanArray() {
+            @Override
+            public boolean get(int key) {
+                if (indexOfKey(key) < 0) {
+                    // Fill the cached locked state as we fetch
+                    put(key, mKeyguardManager.isDeviceLocked(key));
+                }
+                return super.get(key);
+            }
+        };
+
+        int taskCount = rawTasks.size();
+        for (int i = 0; i < taskCount; i++) {
+            ActivityManager.RecentTaskInfo rawTask = rawTasks.get(i);
+            RecentTaskInfoCompat t = new RecentTaskInfoCompat(rawTask);
+            Task.TaskKey taskKey = new Task.TaskKey(rawTask);
+            Task task;
+            if (!loadKeysOnly) {
+                ActivityManager.TaskDescription rawTd = t.getTaskDescription();
+                TaskDescriptionCompat td = new TaskDescriptionCompat(rawTd);
+                boolean isLocked = tmpLockedUsers.get(t.getUserId());
+                task = new Task(taskKey, td.getPrimaryColor(), td.getBackgroundColor(),
+                        t.supportsSplitScreenMultiWindow(), isLocked, rawTd, t.getTopActivity());
+            } else {
+                task = new Task(taskKey);
+            }
+            allTasks.add(task);
+        }
+
+        return allTasks;
+    }
+}
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index b93a54b..ef735e1 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -219,7 +219,6 @@
         // onActivityStart callback.
         mFallbackRecentsView.setContentAlpha(1);
         super.onStart();
-        UiFactory.onStart(this);
         mFallbackRecentsView.resetTaskVisuals();
     }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 517f759..a9ce5cc 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -18,38 +18,22 @@
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
 
 import android.annotation.TargetApi;
-import android.app.ActivityManager;
 import android.content.ComponentCallbacks2;
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.util.Log;
-import android.util.LruCache;
 import android.util.SparseArray;
-import android.view.accessibility.AccessibilityManager;
-
 import com.android.launcher3.MainThreadExecutor;
-import com.android.launcher3.R;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.UiThreadHelper;
 import com.android.systemui.shared.recents.ISystemUiProxy;
-import com.android.systemui.shared.recents.model.IconLoader;
-import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
-import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan.PreloadOptions;
-import com.android.systemui.shared.recents.model.RecentsTaskLoader;
-import com.android.systemui.shared.recents.model.TaskKeyLruCache;
+import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.BackgroundExecutor;
 import com.android.systemui.shared.system.TaskStackChangeListener;
-
 import java.util.ArrayList;
 import java.util.function.Consumer;
 
@@ -68,110 +52,102 @@
     private final ArrayList<AssistDataListener> mAssistDataListeners = new ArrayList<>();
 
     private final Context mContext;
-    private final RecentsTaskLoader mRecentsTaskLoader;
     private final MainThreadExecutor mMainThreadExecutor;
-    private final Handler mBgHandler;
 
-    private RecentsTaskLoadPlan mLastLoadPlan;
-    private int mLastLoadPlanId;
-    private int mTaskChangeId;
     private ISystemUiProxy mSystemUiProxy;
     private boolean mClearAssistCacheOnStackChange = true;
-    private final boolean mIsLowRamDevice;
-    private boolean mPreloadTasksInBackground;
-    private final AccessibilityManager mAccessibilityManager;
+
+    private final RecentTasksList mTaskList;
+    private final TaskIconCache mIconCache;
+    private final TaskThumbnailCache mThumbnailCache;
 
     private RecentsModel(Context context) {
         mContext = context;
 
-        ActivityManager activityManager =
-                (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
-        mIsLowRamDevice = activityManager.isLowRamDevice();
         mMainThreadExecutor = new MainThreadExecutor();
-        mBgHandler = new Handler(UiThreadHelper.getBackgroundLooper());
 
-        Resources res = context.getResources();
-        mRecentsTaskLoader = new RecentsTaskLoader(mContext,
-                res.getInteger(R.integer.config_recentsMaxThumbnailCacheSize),
-                res.getInteger(R.integer.config_recentsMaxIconCacheSize), 0) {
-
-            @Override
-            protected IconLoader createNewIconLoader(Context context,
-                    TaskKeyLruCache<Drawable> iconCache,
-                    LruCache<ComponentName, ActivityInfo> activityInfoCache) {
-                // Disable finding the dominant color since we don't need to use it
-                return new NormalizedIconLoader(context, iconCache, activityInfoCache,
-                        true /* disableColorExtraction */);
-            }
-        };
-        mRecentsTaskLoader.startLoader(mContext);
+        HandlerThread loaderThread = new HandlerThread("TaskThumbnailIconCache",
+                Process.THREAD_PRIORITY_BACKGROUND);
+        loaderThread.start();
+        mTaskList = new RecentTasksList(context);
+        mIconCache = new TaskIconCache(context, loaderThread.getLooper());
+        mThumbnailCache = new TaskThumbnailCache(context, loaderThread.getLooper());
         ActivityManagerWrapper.getInstance().registerTaskStackListener(this);
-
-        mTaskChangeId = 1;
-        loadTasks(-1, null);
-        mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
     }
 
-    public RecentsTaskLoader getRecentsTaskLoader() {
-        return mRecentsTaskLoader;
+    public TaskIconCache getIconCache() {
+        return mIconCache;
+    }
+
+    public TaskThumbnailCache getThumbnailCache() {
+        return mThumbnailCache;
     }
 
     /**
-     * Preloads the task plan
-     * @param taskId The running task id or -1
+     * Fetches the list of recent tasks.
+     *
      * @param callback The callback to receive the task plan once its complete or null. This is
      *                always called on the UI thread.
      * @return the request id associated with this call.
      */
-    public int loadTasks(int taskId, Consumer<RecentsTaskLoadPlan> callback) {
-        final int requestId = mTaskChangeId;
+    public int getTasks(Consumer<ArrayList<Task>> callback) {
+        return mTaskList.getTasks(-1, false /* loadKeysOnly */, callback);
+    }
 
-        // Fail fast if nothing has changed.
-        if (mLastLoadPlanId == mTaskChangeId) {
-            if (callback != null) {
-                final RecentsTaskLoadPlan plan = mLastLoadPlan;
-                mMainThreadExecutor.execute(() -> callback.accept(plan));
+    /**
+     * @return Whether the provided {@param changeId} is the latest recent tasks list id.
+     */
+    public boolean isTaskListValid(int changeId) {
+        return mTaskList.isTaskListValid(changeId);
+    }
+
+    /**
+     * Finds and returns the task key associated with the given task id.
+     *
+     * @param callback The callback to receive the task key if it is found or null. This is always
+     *                 called on the UI thread.
+     */
+    public void findTaskWithId(int taskId, Consumer<Task.TaskKey> callback) {
+        mTaskList.getTasks(-1, true /* loadKeysOnly */, (tasks) -> {
+            for (Task task : tasks) {
+                if (task.key.id == taskId) {
+                    callback.accept(task.key);
+                    return;
+                }
             }
-            return requestId;
+            callback.accept(null);
+        });
+    }
+
+    @Override
+    public void onTaskStackChangedBackground() {
+        if (!mThumbnailCache.isPreloadingEnabled()) {
+            // Skip if we aren't preloading
+            return;
         }
 
-        BackgroundExecutor.get().submit(() -> {
-            // Preload the plan
-            RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(mContext);
-            PreloadOptions opts = new PreloadOptions();
-            opts.loadTitles = mAccessibilityManager.isEnabled();
-            loadPlan.preloadPlan(opts, mRecentsTaskLoader, taskId, UserHandle.myUserId());
-            // Set the load plan on UI thread
-            mMainThreadExecutor.execute(() -> {
-                mLastLoadPlan = loadPlan;
-                mLastLoadPlanId = requestId;
+        int currentUserId = Process.myUserHandle().getIdentifier();
+        if (!checkCurrentOrManagedUserId(currentUserId, mContext)) {
+            // Skip if we are not the current user
+            return;
+        }
 
-                if (callback != null) {
-                    callback.accept(loadPlan);
+        // Keep the cache up to date with the latest thumbnails
+        mTaskList.getTasks(mThumbnailCache.getCacheSize(), true /* keysOnly */, (tasks) -> {
+            int runningTaskId = ActivityManagerWrapper.getInstance().getRunningTask().id;
+            for (Task task : tasks) {
+                if (task.key.id == runningTaskId) {
+                    // Skip the running task, it's not going to have an up-to-date snapshot by the
+                    // time the user next enters overview
+                    continue;
                 }
-            });
+                mThumbnailCache.updateThumbnailInCache(task);
+            }
         });
-        return requestId;
-    }
-
-    public void setPreloadTasksInBackground(boolean preloadTasksInBackground) {
-        mPreloadTasksInBackground = preloadTasksInBackground && !mIsLowRamDevice;
-    }
-
-    @Override
-    public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
-        mTaskChangeId++;
-    }
-
-    @Override
-    public void onActivityUnpinned() {
-        mTaskChangeId++;
     }
 
     @Override
     public void onTaskStackChanged() {
-        mTaskChangeId++;
-
         Preconditions.assertUIThread();
         if (mClearAssistCacheOnStackChange) {
             mCachedAssistData.clear();
@@ -180,39 +156,6 @@
         }
     }
 
-    @Override
-    public void onTaskStackChangedBackground() {
-        int userId = UserHandle.myUserId();
-        if (!mPreloadTasksInBackground || !checkCurrentOrManagedUserId(userId, mContext)) {
-            // TODO: Only register this for the current user
-            return;
-        }
-
-        // Preload a fixed number of task icons/thumbnails in the background
-        ActivityManager.RunningTaskInfo runningTaskInfo =
-                ActivityManagerWrapper.getInstance().getRunningTask();
-        RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(mContext);
-        RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
-        launchOpts.runningTaskId = runningTaskInfo != null ? runningTaskInfo.id : -1;
-        launchOpts.numVisibleTasks = 2;
-        launchOpts.numVisibleTaskThumbnails = 2;
-        launchOpts.onlyLoadForCache = true;
-        launchOpts.onlyLoadPausedActivities = true;
-        launchOpts.loadThumbnails = true;
-        PreloadOptions preloadOpts = new PreloadOptions();
-        preloadOpts.loadTitles = mAccessibilityManager.isEnabled();
-        plan.preloadPlan(preloadOpts, mRecentsTaskLoader, -1, userId);
-        mRecentsTaskLoader.loadTasks(plan, launchOpts);
-    }
-
-    public boolean isLoadPlanValid(int resultId) {
-        return mTaskChangeId == resultId;
-    }
-
-    public RecentsTaskLoadPlan getLastLoadPlan() {
-        return mLastLoadPlan;
-    }
-
     public void setSystemUiProxy(ISystemUiProxy systemUiProxy) {
         mSystemUiProxy = systemUiProxy;
     }
@@ -221,16 +164,15 @@
         return mSystemUiProxy;
     }
 
-    public void onStart() {
-        mRecentsTaskLoader.startLoader(mContext);
-    }
-
     public void onTrimMemory(int level) {
         if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
-            // We already stop the loader in UI_HIDDEN, so stop the high res loader as well
-            mRecentsTaskLoader.getHighResThumbnailLoader().setVisible(false);
+            mThumbnailCache.getHighResLoadingState().setVisible(false);
         }
-        mBgHandler.post(() -> mRecentsTaskLoader.onTrimMemory(level));
+        if (level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
+            // Clear everything once we reach a low-mem situation
+            mThumbnailCache.clear();
+            mIconCache.clear();
+        }
     }
 
     public void onOverviewShown(boolean fromHome, String tag) {
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
new file mode 100644
index 0000000..afa58fa
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -0,0 +1,151 @@
+/*
+ * 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 android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.LruCache;
+import android.view.accessibility.AccessibilityManager;
+import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.icons.HandlerRunnable;
+import com.android.launcher3.util.Preconditions;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.TaskKeyLruCache;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import java.util.function.Consumer;
+
+/**
+ * Manages the caching of task icons and related data.
+ * TODO: This class should later be merged into IconCache.
+ */
+public class TaskIconCache {
+
+    private final Handler mBackgroundHandler;
+    private final MainThreadExecutor mMainThreadExecutor;
+    private final AccessibilityManager mAccessibilityManager;
+
+    private final NormalizedIconLoader mIconLoader;
+
+    private final TaskKeyLruCache<Drawable> mIconCache;
+    private final TaskKeyLruCache<String> mContentDescriptionCache;
+    private final LruCache<ComponentName, ActivityInfo> mActivityInfoCache;
+
+    private TaskKeyLruCache.EvictionCallback mClearActivityInfoOnEviction =
+            new TaskKeyLruCache.EvictionCallback() {
+        @Override
+        public void onEntryEvicted(Task.TaskKey key) {
+            if (key != null) {
+                mActivityInfoCache.remove(key.getComponent());
+            }
+        }
+    };
+
+    public TaskIconCache(Context context, Looper backgroundLooper) {
+        mBackgroundHandler = new Handler(backgroundLooper);
+        mMainThreadExecutor = new MainThreadExecutor();
+        mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+
+        Resources res = context.getResources();
+        int cacheSize = res.getInteger(R.integer.recentsIconCacheSize);
+        mIconCache = new TaskKeyLruCache<>(cacheSize, mClearActivityInfoOnEviction);
+        mContentDescriptionCache = new TaskKeyLruCache<>(cacheSize, mClearActivityInfoOnEviction);
+        mActivityInfoCache = new LruCache<>(cacheSize);
+        mIconLoader = new NormalizedIconLoader(context, mIconCache, mActivityInfoCache,
+                true /* disableColorExtraction */);
+    }
+
+    /**
+     * 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
+     */
+    public IconLoadRequest updateIconInBackground(Task task, Consumer<Task> callback) {
+        Preconditions.assertUIThread();
+        if (task.icon != null) {
+            // Nothing to load, the icon is already loaded
+            callback.accept(task);
+            return null;
+        }
+
+        IconLoadRequest request = new IconLoadRequest(mBackgroundHandler) {
+            @Override
+            public void run() {
+                Drawable icon = mIconLoader.getIcon(task);
+                String contentDescription = loadContentDescriptionInBackground(task);
+                if (isCanceled()) {
+                    // We don't call back to the provided callback in this case
+                    return;
+                }
+                mMainThreadExecutor.execute(() -> {
+                    task.icon = icon;
+                    task.titleDescription = contentDescription;
+                    callback.accept(task);
+                    onEnd();
+                });
+            }
+        };
+        Utilities.postAsyncCallback(mBackgroundHandler, request);
+        return request;
+    }
+
+    public void clear() {
+        mIconCache.evictAll();
+        mContentDescriptionCache.evictAll();
+    }
+
+    /**
+     * Loads the content description for the given {@param task}.
+     */
+    private String loadContentDescriptionInBackground(Task task) {
+        // Return the cached content description if it exists
+        String label = mContentDescriptionCache.getAndInvalidateIfModified(task.key);
+        if (label != null) {
+            return label;
+        }
+
+        // Skip loading content descriptions if accessibility is not enabled
+        if (!mAccessibilityManager.isEnabled()) {
+            return "";
+        }
+
+        // Skip loading the content description if the activity no longer exists
+        ActivityInfo activityInfo = mIconLoader.getAndUpdateActivityInfo(task.key);
+        if (activityInfo == null) {
+            return "";
+        }
+
+        // Load the label otherwise
+        label = ActivityManagerWrapper.getInstance().getBadgedContentDescription(activityInfo,
+                task.key.userId, task.taskDescription);
+        mContentDescriptionCache.put(task.key, label);
+        return label;
+    }
+
+    public static abstract class IconLoadRequest extends HandlerRunnable {
+        IconLoadRequest(Handler handler) {
+            super(handler, null);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
new file mode 100644
index 0000000..c47101b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -0,0 +1,198 @@
+/*
+ * 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 android.app.ActivityManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.os.Looper;
+import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.icons.HandlerRunnable;
+import com.android.launcher3.util.Preconditions;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.TaskKeyLruCache;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+public class TaskThumbnailCache {
+
+    private final Handler mBackgroundHandler;
+    private final MainThreadExecutor mMainThreadExecutor;
+
+    private final int mCacheSize;
+    private final TaskKeyLruCache<ThumbnailData> mCache;
+    private final HighResLoadingState mHighResLoadingState;
+
+    public static class HighResLoadingState {
+        private boolean mIsLowRamDevice;
+        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) {
+            ActivityManager activityManager =
+                    (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+            mIsLowRamDevice = activityManager.isLowRamDevice();
+        }
+
+        public void addCallback(HighResLoadingStateChangedCallback callback) {
+            mCallbacks.add(callback);
+        }
+
+        public void removeCallback(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 = !mIsLowRamDevice && mVisible && !mFlingingFast;
+            if (prevState != mHighResLoadingEnabled) {
+                for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+                    mCallbacks.get(i).onHighResLoadingStateChanged(mHighResLoadingEnabled);
+                }
+            }
+        }
+    }
+
+    public TaskThumbnailCache(Context context, Looper backgroundLooper) {
+        mBackgroundHandler = new Handler(backgroundLooper);
+        mMainThreadExecutor = new MainThreadExecutor();
+        mHighResLoadingState = new HighResLoadingState(context);
+
+        Resources res = context.getResources();
+        mCacheSize = res.getInteger(R.integer.recentsThumbnailCacheSize);
+        mCache = new TaskKeyLruCache<>(mCacheSize);
+    }
+
+    /**
+     * Synchronously fetches the thumbnail for the given {@param task} and puts it in the cache.
+     */
+    public void updateThumbnailInCache(Task task) {
+        Preconditions.assertUIThread();
+
+        // Fetch the thumbnail for this task and put it in the cache
+        mCache.put(task.key, ActivityManagerWrapper.getInstance().getTaskThumbnail(
+                task.key.id, true /* reducedResolution */));
+    }
+
+
+    /**
+     * Asynchronously fetches the icon and other task data for the given {@param task}.
+     *
+     * @param callback The callback to receive the task after its data has been populated.
+     * @return A cancelable handle to the request
+     */
+    public ThumbnailLoadRequest updateThumbnailInBackground(Task task, boolean reducedResolution,
+            Consumer<Task> callback) {
+        Preconditions.assertUIThread();
+
+        if (task.thumbnail != null && (!task.thumbnail.reducedResolution || reducedResolution)) {
+            // Nothing to load, the thumbnail is already high-resolution or matches what the
+            // request, so just callback
+            callback.accept(task);
+            return null;
+        }
+
+        ThumbnailData cachedThumbnail = mCache.getAndInvalidateIfModified(task.key);
+        if (cachedThumbnail != null && (!cachedThumbnail.reducedResolution || reducedResolution)) {
+            // Already cached, lets use that thumbnail
+            task.thumbnail = cachedThumbnail;
+            callback.accept(task);
+            return null;
+        }
+
+        ThumbnailLoadRequest request = new ThumbnailLoadRequest(mBackgroundHandler,
+                reducedResolution) {
+            @Override
+            public void run() {
+                ThumbnailData thumbnail = ActivityManagerWrapper.getInstance().getTaskThumbnail(
+                        task.key.id, reducedResolution);
+                if (isCanceled()) {
+                    // We don't call back to the provided callback in this case
+                    return;
+                }
+                mMainThreadExecutor.execute(() -> {
+                    task.thumbnail = thumbnail;
+                    callback.accept(task);
+                    onEnd();
+                });
+            }
+        };
+        Utilities.postAsyncCallback(mBackgroundHandler, request);
+        return request;
+    }
+
+    /**
+     * Clears the cache.
+     */
+    public void clear() {
+        mCache.evictAll();
+    }
+
+    /**
+     * @return The cache size.
+     */
+    public int getCacheSize() {
+        return mCacheSize;
+    }
+
+    /**
+     * @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 !mHighResLoadingState.mIsLowRamDevice && mHighResLoadingState.mVisible;
+    }
+
+    public static abstract class ThumbnailLoadRequest extends HandlerRunnable {
+        public final boolean reducedResolution;
+
+        ThumbnailLoadRequest(Handler handler, boolean reducedResolution) {
+            super(handler, null);
+            this.reducedResolution = reducedResolution;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 9371a4c..b1a214d 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -196,7 +196,6 @@
         super.onCreate();
         mAM = ActivityManagerWrapper.getInstance();
         mRecentsModel = RecentsModel.INSTANCE.get(this);
-        mRecentsModel.setPreloadTasksInBackground(true);
         mMainThreadExecutor = new MainThreadExecutor();
         mOverviewCommandHelper = new OverviewCommandHelper(this);
         mMainThreadChoreographer = Choreographer.getInstance();
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index 1c79f44..6908b89 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -504,8 +504,8 @@
         // This method is only called when STATE_GESTURE_STARTED_QUICKSTEP/
         // STATE_GESTURE_STARTED_QUICKSCRUB is set, so we can enable the high-res thumbnail loader
         // here once we are sure that we will end up in an overview state
-        RecentsModel.INSTANCE.get(mContext).getRecentsTaskLoader()
-                .getHighResThumbnailLoader().setVisible(true);
+        RecentsModel.INSTANCE.get(mContext).getThumbnailCache()
+                .getHighResLoadingState().setVisible(true);
     }
 
     private void shiftAnimationDestinationForQuickscrub() {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 1205bdc..cbbd181 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -43,7 +43,6 @@
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Handler;
-import android.os.UserHandle;
 import android.text.Layout;
 import android.text.StaticLayout;
 import android.text.TextPaint;
@@ -78,13 +77,11 @@
 import com.android.quickstep.OverviewCallbacks;
 import com.android.quickstep.QuickScrubController;
 import com.android.quickstep.RecentsModel;
+import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.util.ClipAnimationHelper;
 import com.android.quickstep.util.TaskViewDrawable;
-import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
-import com.android.systemui.shared.recents.model.RecentsTaskLoader;
 import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.TaskStack;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.BackgroundExecutor;
@@ -100,7 +97,8 @@
  * A list of recent tasks.
  */
 @TargetApi(Build.VERSION_CODES.P)
-public abstract class RecentsView<T extends BaseActivity> extends PagedView implements Insettable {
+public abstract class RecentsView<T extends BaseActivity> extends PagedView implements Insettable,
+        TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback {
 
     private static final String TAG = RecentsView.class.getSimpleName();
 
@@ -206,17 +204,13 @@
                     handler.post(() ->
                             dismissTask(taskView, true /* animate */, false /* removeTask */));
                 } else {
-                    RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(getContext());
-                    RecentsTaskLoadPlan.PreloadOptions opts =
-                            new RecentsTaskLoadPlan.PreloadOptions();
-                    opts.loadTitles = false;
-                    loadPlan.preloadPlan(opts, mModel.getRecentsTaskLoader(), -1,
-                            UserHandle.myUserId());
-                    if (loadPlan.getTaskStack().findTaskWithId(taskId) == null) {
-                        // The task was removed from the recents list
-                        handler.post(() ->
-                                dismissTask(taskView, true /* animate */, false /* removeTask */));
-                    }
+                    mModel.findTaskWithId(taskKey.id, (key) -> {
+                        if (key == null) {
+                            // The task was removed from the recents list
+                            handler.post(() -> dismissTask(taskView, true /* animate */,
+                                    false /* removeTask */));
+                        }
+                    });
                 }
             });
         }
@@ -229,9 +223,9 @@
         }
     };
 
-    // Used to keep track of the last requested load plan id, so that we do not request to load the
+    // Used to keep track of the last requested task list id, so that we do not request to load the
     // tasks again if we have already requested it and the task list has not changed
-    private int mRequestedLoadPlanId = -1;
+    private int mTaskListChangeId = -1;
 
     // Only valid until the launcher state changes to NORMAL
     private int mRunningTaskId = -1;
@@ -285,7 +279,6 @@
         mActivity = (T) BaseActivity.fromContext(context);
         mQuickScrubController = new QuickScrubController(mActivity, this);
         mModel = RecentsModel.INSTANCE.get(context);
-
         mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
                 .inflate(R.layout.overview_clear_all_button, this, false);
         mClearAllButton.setOnClickListener(this::dismissAllTasks);
@@ -316,7 +309,7 @@
     public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData) {
         TaskView taskView = getTaskView(taskId);
         if (taskView != null) {
-            taskView.onTaskDataLoaded(taskView.getTask(), thumbnailData);
+            taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData);
         }
         return taskView;
     }
@@ -331,6 +324,7 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         updateTaskStackListenerState();
+        mModel.getThumbnailCache().getHighResLoadingState().addCallback(this);
         mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
         ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
     }
@@ -339,6 +333,7 @@
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         updateTaskStackListenerState();
+        mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this);
         mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
         ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
     }
@@ -349,12 +344,11 @@
 
         // Clear the task data for the removed child if it was visible
         if (child != mClearAllButton) {
-            Task task = ((TaskView) child).getTask();
+            TaskView taskView = (TaskView) child;
+            Task task = taskView.getTask();
             if (mHasVisibleTaskData.get(task.key.id)) {
                 mHasVisibleTaskData.delete(task.key.id);
-                RecentsTaskLoader loader = mModel.getRecentsTaskLoader();
-                loader.unloadTaskData(task);
-                loader.getHighResThumbnailLoader().onTaskInvisible(task);
+                taskView.onTaskListVisibilityChanged(false /* visible */);
             }
         }
     }
@@ -444,14 +438,13 @@
         return true;
     }
 
-    private void applyLoadPlan(RecentsTaskLoadPlan loadPlan) {
+    private void applyLoadPlan(ArrayList<Task> tasks) {
         if (mPendingAnimation != null) {
-            mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(loadPlan));
+            mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(tasks));
             return;
         }
 
-        TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null;
-        if (stack == null) {
+        if (tasks == null || tasks.isEmpty()) {
             removeAllViews();
             onTaskStackUpdated();
             return;
@@ -462,7 +455,6 @@
         // Ensure there are as many views as there are tasks in the stack (adding and trimming as
         // necessary)
         final LayoutInflater inflater = LayoutInflater.from(getContext());
-        final ArrayList<Task> tasks = new ArrayList<>(stack.getTasks());
 
         // Unload existing visible task data
         unloadVisibleTaskData();
@@ -581,9 +573,8 @@
             loadVisibleTaskData();
         }
 
-        // Update the high res thumbnail loader
-        RecentsTaskLoader loader = mModel.getRecentsTaskLoader();
-        loader.getHighResThumbnailLoader().setFlingingFast(isFlingingFast);
+        // Update the high res thumbnail loader state
+        mModel.getThumbnailCache().getHighResLoadingState().setFlingingFast(isFlingingFast);
         return scrolling;
     }
 
@@ -618,13 +609,12 @@
      * and unloads the associated task data for tasks that are no longer visible.
      */
     public void loadVisibleTaskData() {
-        if (!mOverviewStateEnabled || mRequestedLoadPlanId == -1) {
+        if (!mOverviewStateEnabled || mTaskListChangeId == -1) {
             // Skip loading visible task data if we've already left the overview state, or if the
             // task list hasn't been loaded yet (the task views will not reflect the task list)
             return;
         }
 
-        RecentsTaskLoader loader = mModel.getRecentsTaskLoader();
         int centerPageIndex = getPageNearestToCenterOfScreen();
         int numChildren = getTaskViewCount();
         int lower = Math.max(0, centerPageIndex - 2);
@@ -641,14 +631,12 @@
                     continue;
                 }
                 if (!mHasVisibleTaskData.get(task.key.id)) {
-                    loader.loadTaskData(task);
-                    loader.getHighResThumbnailLoader().onTaskVisible(task);
+                    taskView.onTaskListVisibilityChanged(true /* visible */);
                 }
                 mHasVisibleTaskData.put(task.key.id, visible);
             } else {
                 if (mHasVisibleTaskData.get(task.key.id)) {
-                    loader.unloadTaskData(task);
-                    loader.getHighResThumbnailLoader().onTaskInvisible(task);
+                    taskView.onTaskListVisibilityChanged(false /* visible */);
                 }
                 mHasVisibleTaskData.delete(task.key.id);
             }
@@ -659,27 +647,40 @@
      * Unloads any associated data from the currently visible tasks
      */
     private void unloadVisibleTaskData() {
-        RecentsTaskLoader loader = mModel.getRecentsTaskLoader();
         for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
             if (mHasVisibleTaskData.valueAt(i)) {
                 TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i));
                 if (taskView != null) {
-                    Task task = taskView.getTask();
-                    loader.unloadTaskData(task);
-                    loader.getHighResThumbnailLoader().onTaskInvisible(task);
+                    taskView.onTaskListVisibilityChanged(false /* visible */);
                 }
             }
         }
         mHasVisibleTaskData.clear();
     }
 
+    @Override
+    public void onHighResLoadingStateChanged(boolean enabled) {
+        // 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++) {
+            if (mHasVisibleTaskData.valueAt(i)) {
+                TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i));
+                if (taskView != null) {
+                    // Poke the view again, which will trigger it to load high res if the state
+                    // is enabled
+                    taskView.onTaskListVisibilityChanged(true /* visible */);
+                }
+            }
+        }
+    }
+
     protected abstract void startHome();
 
     public void reset() {
         mRunningTaskId = -1;
         mRunningTaskTileHidden = false;
         mIgnoreResetTaskId = -1;
-        mRequestedLoadPlanId = -1;
+        mTaskListChangeId = -1;
 
         unloadVisibleTaskData();
         setCurrentPage(0);
@@ -691,8 +692,8 @@
      * Reloads the view if anything in recents changed.
      */
     public void reloadIfNeeded() {
-        if (!mModel.isLoadPlanValid(mRequestedLoadPlanId)) {
-            mRequestedLoadPlanId = mModel.loadTasks(mRunningTaskId, this::applyLoadPlan);
+        if (!mModel.isTaskListValid(mTaskListChangeId)) {
+            mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
         }
     }
 
@@ -753,8 +754,8 @@
 
         setCurrentPage(0);
 
-        // Load the tasks
-        reloadIfNeeded();
+        // Load the tasks (if the loading is already
+        mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
     }
 
     public void showNextTask() {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 56074f0..da5b79a 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -17,7 +17,6 @@
 package com.android.quickstep.views;
 
 import static android.widget.Toast.LENGTH_SHORT;
-
 import static com.android.launcher3.BaseActivity.fromContext;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
@@ -30,6 +29,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Outline;
+import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.Handler;
 import android.util.AttributeSet;
@@ -47,14 +47,15 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.TaskIconCache;
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskSystemShortcut;
+import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.views.RecentsView.PageCallbacks;
 import com.android.quickstep.views.RecentsView.ScrollState;
 import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.Task.TaskCallbacks;
-import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
 
@@ -64,7 +65,7 @@
 /**
  * A task in the Recents view.
  */
-public class TaskView extends FrameLayout implements TaskCallbacks, PageCallbacks {
+public class TaskView extends FrameLayout implements PageCallbacks {
 
     private static final String TAG = TaskView.class.getSimpleName();
 
@@ -137,6 +138,10 @@
     private Animator mIconAndDimAnimator;
     private float mFocusTransitionProgress = 1;
 
+    // The current background requests to load the task thumbnail and icon
+    private TaskThumbnailCache.ThumbnailLoadRequest mThumbnailLoadRequest;
+    private TaskIconCache.IconLoadRequest mIconLoadRequest;
+
     public TaskView(Context context) {
         this(context, null);
     }
@@ -170,13 +175,8 @@
      * Updates this task view to the given {@param task}.
      */
     public void bind(Task task) {
-        if (mTask != null) {
-            mTask.removeCallback(this);
-        }
         mTask = task;
         mSnapshotView.bind();
-        task.addCallback(this);
-        setContentDescription(task.titleDescription);
     }
 
     public Task getTask() {
@@ -233,15 +233,34 @@
         }
     }
 
-    @Override
-    public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) {
-        mSnapshotView.setThumbnail(task, thumbnailData);
-        mIconView.setDrawable(task.icon);
-        mIconView.setOnClickListener(icon -> showTaskMenu());
-        mIconView.setOnLongClickListener(icon -> {
-            requestDisallowInterceptTouchEvent(true);
-            return showTaskMenu();
-        });
+    public void onTaskListVisibilityChanged(boolean visible) {
+        if (mTask == null) {
+            return;
+        }
+        if (visible) {
+            // These calls are no-ops if the data is already loaded, try and load the high
+            // resolution thumbnail if the state permits
+            RecentsModel model = RecentsModel.INSTANCE.get(getContext());
+            TaskThumbnailCache thumbnailCache = model.getThumbnailCache();
+            TaskIconCache iconCache = model.getIconCache();
+            mThumbnailLoadRequest = thumbnailCache.updateThumbnailInBackground(mTask,
+                    !thumbnailCache.getHighResLoadingState().isEnabled() /* reducedResolution */,
+                    (task) -> mSnapshotView.setThumbnail(task, task.thumbnail));
+            mIconLoadRequest = iconCache.updateIconInBackground(mTask,
+                    (task) -> {
+                        setContentDescription(task.titleDescription);
+                        setIcon(task.icon);
+                    });
+        } else {
+            if (mThumbnailLoadRequest != null) {
+                mThumbnailLoadRequest.cancel();
+            }
+            if (mIconLoadRequest != null) {
+                mIconLoadRequest.cancel();
+            }
+            mSnapshotView.setThumbnail(null, null);
+            setIcon(null);
+        }
     }
 
     private boolean showTaskMenu() {
@@ -253,16 +272,18 @@
         return mMenuView != null;
     }
 
-    @Override
-    public void onTaskDataUnloaded() {
-        mSnapshotView.setThumbnail(null, null);
-        mIconView.setDrawable(null);
-        mIconView.setOnLongClickListener(null);
-    }
-
-    @Override
-    public void onTaskWindowingModeChanged() {
-        // Do nothing
+    private void setIcon(Drawable icon) {
+        if (icon != null) {
+            mIconView.setDrawable(icon);
+            mIconView.setOnClickListener(v -> showTaskMenu());
+            mIconView.setOnLongClickListener(v -> {
+                requestDisallowInterceptTouchEvent(true);
+                return showTaskMenu();
+            });
+        } else {
+            mIconView.setDrawable(null);
+            mIconView.setOnLongClickListener(null);
+        }
     }
 
     private void setIconAndDimTransitionProgress(float progress) {
diff --git a/res/values/config.xml b/res/values/config.xml
index 85c2e65..946afec 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -132,6 +132,4 @@
 
 <!-- Recents -->
     <item type="id" name="overview_panel"/>
-    <integer name="config_recentsMaxThumbnailCacheSize">6</integer>
-    <integer name="config_recentsMaxIconCacheSize">12</integer>
 </resources>
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index e714a0b..3ae9a49 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -768,7 +768,6 @@
         }
         mAppWidgetHost.setListenIfResumed(true);
         NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
-        UiFactory.onStart(this);
     }
 
     private void logOnDelayedResume() {
diff --git a/src/com/android/launcher3/icons/HandlerRunnable.java b/src/com/android/launcher3/icons/HandlerRunnable.java
new file mode 100644
index 0000000..e7132cd
--- /dev/null
+++ b/src/com/android/launcher3/icons/HandlerRunnable.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.icons;
+
+import android.os.Handler;
+
+/**
+ * A runnable that can be posted to a {@link Handler} which can be canceled.
+ */
+public abstract class HandlerRunnable implements Runnable {
+
+    private final Handler mHandler;
+    private final Runnable mEndRunnable;
+
+    private boolean mEnded = false;
+    private boolean mCanceled = false;
+
+    public HandlerRunnable(Handler handler, Runnable endRunnable) {
+        mHandler = handler;
+        mEndRunnable = endRunnable;
+    }
+
+    /**
+     * Cancels this runnable from being run, only if it has not already run.
+     */
+    public void cancel() {
+        mHandler.removeCallbacks(this);
+        // TODO: This can actually cause onEnd to be called twice if the handler is already running
+        //       this runnable
+        // NOTE: This is currently run on whichever thread the caller is run on.
+        mCanceled = true;
+        onEnd();
+    }
+
+    /**
+     * @return whether this runnable was canceled.
+     */
+    protected boolean isCanceled() {
+        return mCanceled;
+    }
+
+    /**
+     * To be called by the implemention of this runnable. The end callback is done on whichever
+     * thread the caller is calling from.
+     */
+    public void onEnd() {
+        if (!mEnded) {
+            mEnded = true;
+            if (mEndRunnable != null) {
+                mEndRunnable.run();
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index 41a53e5..6e2ca28 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -169,27 +169,9 @@
         applyCacheEntry(entry, infoInOut);
     }
 
-    public static abstract class IconLoadRequest implements Runnable {
-        private final Handler mHandler;
-        private final Runnable mEndRunnable;
-
-        private boolean mEnded = false;
-
+    public static abstract class IconLoadRequest extends HandlerRunnable {
         IconLoadRequest(Handler handler, Runnable endRunnable) {
-            mHandler = handler;
-            mEndRunnable = endRunnable;
-        }
-
-        public void cancel() {
-            mHandler.removeCallbacks(this);
-            onEnd();
-        }
-
-        public void onEnd() {
-            if (!mEnded) {
-                mEnded = true;
-                mEndRunnable.run();
-            }
+            super(handler, endRunnable);
         }
     }