Merge "Increase the touch slop for noti shade pull down. Bug: 116879058" into ub-launcher3-master
diff --git a/Android.bp b/Android.bp
index 4b32702..e3dd5e5 100644
--- a/Android.bp
+++ b/Android.bp
@@ -16,7 +16,8 @@
     name: "launcher-aosp-tapl",
     static_libs: [
         "androidx.annotation_annotation",
-        "androidx-test",
+        "androidx.test.runner",
+        "androidx.test.rules",
         "androidx.test.uiautomator_uiautomator",
         "SystemUISharedLib",
     ],
diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar
index c5a7c05..6f3fa4b 100644
--- a/quickstep/libs/sysui_shared.jar
+++ b/quickstep/libs/sysui_shared.jar
Binary files differ
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..f8f0905 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -227,10 +227,10 @@
         public RecentsActivityCommand() {
             mHelper = getActivityControlHelper();
             mCreateTime = SystemClock.elapsedRealtime();
-            mRunningTaskId = mAM.getRunningTask().id;
+            mRunningTaskId = RecentsModel.getRunningTaskId();
 
             // 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..e184a9b 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -20,36 +20,21 @@
 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 +53,111 @@
     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 The task id of the running task, or -1 if there is no current running task.
+     */
+    public static int getRunningTaskId() {
+        ActivityManager.RunningTaskInfo runningTask =
+                ActivityManagerWrapper.getInstance().getRunningTask();
+        return runningTask != null ? runningTask.id : -1;
+    }
+
+    /**
+     * @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 = RecentsModel.getRunningTaskId();
+            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 +166,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 +174,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/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index f3f2238..0b2f4d9 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.config.FeatureFlags.APPLY_CONFIG_AT_RUNTIME;
+
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -23,6 +25,7 @@
 import android.content.res.XmlResourceParser;
 import android.graphics.Point;
 import android.util.DisplayMetrics;
+import android.util.Log;
 import android.util.Xml;
 import android.view.Display;
 import android.view.WindowManager;
@@ -45,13 +48,13 @@
 
     // We do not need any synchronization for this variable as its only written on UI thread.
     public static final MainThreadInitializedObject<InvariantDeviceProfile> INSTANCE =
-            new MainThreadInitializedObject<>((c) -> {
-                new ConfigMonitor(c).register();
-                return new InvariantDeviceProfile(c);
-            });
+            new MainThreadInitializedObject<>(InvariantDeviceProfile::new);
 
     private static final float ICON_SIZE_DEFINED_IN_APP_DP = 48;
 
+    public static final int CHANGE_FLAG_GRID = 1 << 0;
+    public static final int CHANGE_FLAG_ICON_SIZE = 1 << 1;
+
     // Constants that affects the interpolation curve between statically defined device profile
     // buckets.
     private static float KNEARESTNEIGHBOR = 3;
@@ -61,9 +64,9 @@
     private static float WEIGHT_EFFICIENT = 100000f;
 
     // Profile-defining invariant properties
-    String name;
-    float minWidthDps;
-    float minHeightDps;
+    private String name;
+    private float minWidthDps;
+    private float minHeightDps;
 
     /**
      * Number of icons per row and column in the workspace.
@@ -95,9 +98,11 @@
 
     public Point defaultWallpaperSize;
 
+    private final ArrayList<OnIDPChangeListener> mChangeListeners = new ArrayList<>();
+    private ConfigMonitor mConfigMonitor;
+
     @VisibleForTesting
-    public InvariantDeviceProfile() {
-    }
+    public InvariantDeviceProfile() {}
 
     private InvariantDeviceProfile(InvariantDeviceProfile p) {
         this(p.name, p.minWidthDps, p.minHeightDps, p.numRows, p.numColumns,
@@ -125,6 +130,12 @@
 
     @TargetApi(23)
     private InvariantDeviceProfile(Context context) {
+        initGrid(context);
+        mConfigMonitor = new ConfigMonitor(context,
+                APPLY_CONFIG_AT_RUNTIME.get() ? this::onConfigChanged : this::killProcess);
+    }
+
+    private void initGrid(Context context) {
         WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
         Display display = wm.getDefaultDisplay();
         DisplayMetrics dm = new DisplayMetrics();
@@ -185,6 +196,44 @@
         }
     }
 
+    public void addOnChangeListener(OnIDPChangeListener listener) {
+        mChangeListeners.add(listener);
+    }
+
+    private void killProcess(Context context) {
+        Log.e("ConfigMonitor", "restarting launcher");
+        android.os.Process.killProcess(android.os.Process.myPid());
+    }
+
+    private void onConfigChanged(Context context) {
+        // Config changes, what shall we do?
+        InvariantDeviceProfile oldProfile = new InvariantDeviceProfile(this);
+
+        // Re-init grid
+        initGrid(context);
+
+        int changeFlags = 0;
+        if (numRows != oldProfile.numRows ||
+                numColumns != oldProfile.numColumns ||
+                numFolderColumns != oldProfile.numFolderColumns ||
+                numFolderRows != oldProfile.numFolderRows ||
+                numHotseatIcons != oldProfile.numHotseatIcons) {
+            changeFlags |= CHANGE_FLAG_GRID;
+        }
+
+        if (iconSize != oldProfile.iconSize || iconBitmapSize != oldProfile.iconBitmapSize) {
+            changeFlags |= CHANGE_FLAG_ICON_SIZE;
+        }
+
+        // Create a new config monitor
+        mConfigMonitor.unregister();
+        mConfigMonitor = new ConfigMonitor(context, this::onConfigChanged);
+
+        for (OnIDPChangeListener listener : mChangeListeners) {
+            listener.onIdpChanged(changeFlags, this);
+        }
+    }
+
     ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles(Context context) {
         ArrayList<InvariantDeviceProfile> profiles = new ArrayList<>();
         try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
@@ -356,4 +405,8 @@
         return x * aspectRatio + y;
     }
 
+    public interface OnIDPChangeListener {
+
+        void onIdpChanged(int changeFlags, InvariantDeviceProfile profile);
+    }
 }
\ No newline at end of file
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/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 6bf5812..338c20b 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -17,6 +17,7 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
+import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_SIZE;
 
 import android.content.ComponentName;
 import android.content.ContentProviderClient;
@@ -30,6 +31,7 @@
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Preconditions;
@@ -94,6 +96,7 @@
 
         mContext.registerReceiver(mModel, filter);
         UserManagerCompat.getInstance(mContext).enableAndResetCache();
+        mInvariantDeviceProfile.addOnChangeListener(this::onIdpChanged);
 
         if (!mContext.getResources().getBoolean(R.bool.notification_badging_enabled)) {
             mNotificationBadgingObserver = null;
@@ -113,6 +116,19 @@
         }
     }
 
+    private void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) {
+        if (changeFlags == 0) {
+            return;
+        }
+
+        if ((changeFlags & CHANGE_FLAG_ICON_SIZE) != 0) {
+            LauncherIcons.clearPool();
+            mIconCache.updateIconParams(idp.fillResIconDpi, idp.iconBitmapSize);
+        }
+
+        mModel.forceReload();
+    }
+
     /**
      * Call from Application.onTerminate(), which is not guaranteed to ever be called.
      */
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index fa4ff75..64b5652 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -87,6 +87,12 @@
     // trying to make them fit the orientation the device is in.
     public static final boolean OVERVIEW_USE_SCREENSHOT_ORIENTATION = true;
 
+    /**
+     * Feature flag to handle define config changes dynamically instead of killing the process.
+     */
+    public static final TogglableFlag APPLY_CONFIG_AT_RUNTIME = new TogglableFlag(
+            "APPLY_CONFIG_AT_RUNTIME", false, "Apply display changes dynamically");
+
     public static void initialize(Context context) {
         // Avoid the disk read for user builds
         if (Utilities.IS_DEBUG_DEVICE) {
@@ -191,5 +197,4 @@
             return h$;
         }
     }
-
 }
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index b6a8b50..8f223a3 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -232,7 +232,7 @@
                             nDr = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null);
                         }
                         Utilities.scaleRectAboutCenter(bounds,
-                                li.getNormalizer().getScale(nDr, null, null, null));
+                                li.getNormalizer().getScale(nDr, null));
                     }
                     AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) dr;
 
diff --git a/src/com/android/launcher3/icons/BaseIconCache.java b/src/com/android/launcher3/icons/BaseIconCache.java
index 9198c24..2afe713 100644
--- a/src/com/android/launcher3/icons/BaseIconCache.java
+++ b/src/com/android/launcher3/icons/BaseIconCache.java
@@ -90,11 +90,11 @@
     private final HashMap<ComponentKey, CacheEntry> mCache =
             new HashMap<>(INITIAL_ICON_CACHE_CAPACITY);
     private final InstantAppResolver mInstantAppResolver;
-    final int mIconDpi;
-
-    final IconDB mIconDb;
     final Handler mWorkerHandler;
 
+    int mIconDpi;
+    IconDB mIconDb;
+
     private final BitmapFactory.Options mDecodeOptions;
 
     public BaseIconCache(Context context, int iconDpi, int iconPixelSize) {
@@ -103,8 +103,6 @@
         mUserManager = UserManagerCompat.getInstance(mContext);
         mLauncherApps = LauncherAppsCompat.getInstance(mContext);
         mInstantAppResolver = InstantAppResolver.newInstance(mContext);
-        mIconDpi = iconDpi;
-        mIconDb = new IconDB(context, iconPixelSize);
 
         mIconProvider = IconProvider.newInstance(context);
         mWorkerHandler = new Handler(LauncherModel.getWorkerLooper());
@@ -115,6 +113,22 @@
         } else {
             mDecodeOptions = null;
         }
+
+        mIconDpi = iconDpi;
+        mIconDb = new IconDB(context, iconPixelSize);
+    }
+
+    public void updateIconParams(int iconDpi, int iconPixelSize) {
+        mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize));
+    }
+
+    private synchronized void updateIconParamsBg(int iconDpi, int iconPixelSize) {
+        mIconDpi = iconDpi;
+        mDefaultIcons.clear();
+
+        mIconDb.close();
+        mIconDb = new IconDB(mContext, iconPixelSize);
+        mCache.clear();
     }
 
     private Drawable getFullResDefaultActivityIcon() {
diff --git a/src/com/android/launcher3/icons/BaseIconFactory.java b/src/com/android/launcher3/icons/BaseIconFactory.java
index c8c9618..cd60de5 100644
--- a/src/com/android/launcher3/icons/BaseIconFactory.java
+++ b/src/com/android/launcher3/icons/BaseIconFactory.java
@@ -192,18 +192,18 @@
             }
             AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon;
             dr.setBounds(0, 0, 1, 1);
-            scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape);
-            if (ATLEAST_OREO && !outShape[0] && !(icon instanceof AdaptiveIconDrawable)) {
+            scale = getNormalizer().getScale(icon, outIconBounds);
+            if (ATLEAST_OREO && !(icon instanceof AdaptiveIconDrawable)) {
                 FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground());
                 fsd.setDrawable(icon);
                 fsd.setScale(scale);
                 icon = dr;
-                scale = getNormalizer().getScale(icon, outIconBounds, null, null);
+                scale = getNormalizer().getScale(icon, outIconBounds);
 
                 ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor);
             }
         } else {
-            scale = getNormalizer().getScale(icon, outIconBounds, null, null);
+            scale = getNormalizer().getScale(icon, outIconBounds);
         }
 
         outScale[0] = scale;
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);
         }
     }
 
diff --git a/src/com/android/launcher3/icons/IconNormalizer.java b/src/com/android/launcher3/icons/IconNormalizer.java
index 4052a55..8eb8252 100644
--- a/src/com/android/launcher3/icons/IconNormalizer.java
+++ b/src/com/android/launcher3/icons/IconNormalizer.java
@@ -20,16 +20,13 @@
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
-import android.graphics.Matrix;
 import android.graphics.Paint;
-import android.graphics.Path;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.AdaptiveIconDrawable;
 import android.graphics.drawable.Drawable;
-import android.util.Log;
 
 import java.nio.ByteBuffer;
 
@@ -53,9 +50,6 @@
 
     private static final int MIN_VISIBLE_ALPHA = 40;
 
-    // Shape detection related constants
-    private static final float BOUND_RATIO_MARGIN = .05f;
-    private static final float PIXEL_DIFF_PERCENTAGE_THRESHOLD = 0.005f;
     private static final float SCALE_NOT_INITIALIZED = 0;
 
     // Ratio of the diameter of an normalized circular icon to the actual icon size.
@@ -64,8 +58,6 @@
     private final int mMaxSize;
     private final Bitmap mBitmap;
     private final Canvas mCanvas;
-    private final Paint mPaintMaskShape;
-    private final Paint mPaintMaskShapeOutline;
     private final byte[] mPixels;
 
     private final Rect mAdaptiveIconBounds;
@@ -75,8 +67,6 @@
     private final float[] mLeftBorder;
     private final float[] mRightBorder;
     private final Rect mBounds;
-    private final Path mShapePath;
-    private final Matrix mMatrix;
 
     /** package private **/
     IconNormalizer(Context context, int iconBitmapSize) {
@@ -90,89 +80,10 @@
         mBounds = new Rect();
         mAdaptiveIconBounds = new Rect();
 
-        mPaintMaskShape = new Paint();
-        mPaintMaskShape.setColor(Color.RED);
-        mPaintMaskShape.setStyle(Paint.Style.FILL);
-        mPaintMaskShape.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR));
-
-        mPaintMaskShapeOutline = new Paint();
-        mPaintMaskShapeOutline.setStrokeWidth(2 * context.getResources().getDisplayMetrics().density);
-        mPaintMaskShapeOutline.setStyle(Paint.Style.STROKE);
-        mPaintMaskShapeOutline.setColor(Color.BLACK);
-        mPaintMaskShapeOutline.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
-
-        mShapePath = new Path();
-        mMatrix = new Matrix();
         mAdaptiveIconScale = SCALE_NOT_INITIALIZED;
     }
 
     /**
-     * Returns if the shape of the icon is same as the path.
-     * For this method to work, the shape path bounds should be in [0,1]x[0,1] bounds.
-     */
-    private boolean isShape(Path maskPath) {
-        // Condition1:
-        // If width and height of the path not close to a square, then the icon shape is
-        // not same as the mask shape.
-        float iconRatio = ((float) mBounds.width()) / mBounds.height();
-        if (Math.abs(iconRatio - 1) > BOUND_RATIO_MARGIN) {
-            if (DEBUG) {
-                Log.d(TAG, "Not same as mask shape because width != height. " + iconRatio);
-            }
-            return false;
-        }
-
-        // Condition 2:
-        // Actual icon (white) and the fitted shape (e.g., circle)(red) XOR operation
-        // should generate transparent image, if the actual icon is equivalent to the shape.
-
-        // Fit the shape within the icon's bounding box
-        mMatrix.reset();
-        mMatrix.setScale(mBounds.width(), mBounds.height());
-        mMatrix.postTranslate(mBounds.left, mBounds.top);
-        maskPath.transform(mMatrix, mShapePath);
-
-        // XOR operation
-        mCanvas.drawPath(mShapePath, mPaintMaskShape);
-
-        // DST_OUT operation around the mask path outline
-        mCanvas.drawPath(mShapePath, mPaintMaskShapeOutline);
-
-        // Check if the result is almost transparent
-        return isTransparentBitmap();
-    }
-
-    /**
-     * Used to determine if certain the bitmap is transparent.
-     */
-    private boolean isTransparentBitmap() {
-        ByteBuffer buffer = ByteBuffer.wrap(mPixels);
-        buffer.rewind();
-        mBitmap.copyPixelsToBuffer(buffer);
-
-        int y = mBounds.top;
-        // buffer position
-        int index = y * mMaxSize;
-        // buffer shift after every row, width of buffer = mMaxSize
-        int rowSizeDiff = mMaxSize - mBounds.right;
-
-        int sum = 0;
-        for (; y < mBounds.bottom; y++) {
-            index += mBounds.left;
-            for (int x = mBounds.left; x < mBounds.right; x++) {
-                if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) {
-                    sum++;
-                }
-                index++;
-            }
-            index += rowSizeDiff;
-        }
-
-        float percentageDiffPixels = ((float) sum) / (mBounds.width() * mBounds.height());
-        return percentageDiffPixels < PIXEL_DIFF_PERCENTAGE_THRESHOLD;
-    }
-
-    /**
      * Returns the amount by which the {@param d} should be scaled (in both dimensions) so that it
      * matches the design guidelines for a launcher icon.
      *
@@ -186,8 +97,7 @@
      *
      * @param outBounds optional rect to receive the fraction distance from each edge.
      */
-    public synchronized float getScale(@NonNull Drawable d, @Nullable RectF outBounds,
-            @Nullable Path path, @Nullable boolean[] outMaskShape) {
+    public synchronized float getScale(@NonNull Drawable d, @Nullable RectF outBounds) {
         if (BaseIconFactory.ATLEAST_OREO && d instanceof AdaptiveIconDrawable) {
             if (mAdaptiveIconScale != SCALE_NOT_INITIALIZED) {
                 if (outBounds != null) {
@@ -298,10 +208,6 @@
                     1 - ((float) mBounds.right) / width,
                     1 - ((float) mBounds.bottom) / height);
         }
-
-        if (outMaskShape != null && outMaskShape.length > 0) {
-            outMaskShape[0] = isShape(path);
-        }
         float areaScale = area / (width * height);
         // Use sqrt of the final ratio as the images is scaled across both width and height.
         float scale = areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1;
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index 69614a3..c96d35d 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -27,11 +27,11 @@
 
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.FastBitmapDrawable;
-import com.android.launcher3.graphics.BitmapRenderer;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.graphics.BitmapRenderer;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutInfoCompat;
@@ -48,13 +48,14 @@
 
     private static final Object sPoolSync = new Object();
     private static LauncherIcons sPool;
-    private LauncherIcons next;
+    private static int sPoolId = 0;
 
     /**
      * Return a new Message instance from the global pool. Allows us to
      * avoid allocating new objects in many cases.
      */
     public static LauncherIcons obtain(Context context) {
+        int poolId;
         synchronized (sPoolSync) {
             if (sPool != null) {
                 LauncherIcons m = sPool;
@@ -62,9 +63,33 @@
                 m.next = null;
                 return m;
             }
+            poolId = sPoolId;
         }
+
         InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
-        return new LauncherIcons(context, idp.fillResIconDpi, idp.iconBitmapSize);
+        return new LauncherIcons(context, idp.fillResIconDpi, idp.iconBitmapSize, poolId);
+    }
+
+    public static void clearPool() {
+        synchronized (sPoolSync) {
+            sPool = null;
+            sPoolId++;
+        }
+    }
+
+    private final Context mContext;
+    private final int mFillResIconDpi;
+    private final int mIconBitmapSize;
+    private final int mPoolId;
+
+    private LauncherIcons next;
+
+    private LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize, int poolId) {
+        super(context, fillResIconDpi, iconBitmapSize);
+        mContext = context.getApplicationContext();
+        mFillResIconDpi = fillResIconDpi;
+        mIconBitmapSize = iconBitmapSize;
+        mPoolId = poolId;
     }
 
     /**
@@ -72,6 +97,9 @@
      */
     public void recycle() {
         synchronized (sPoolSync) {
+            if (sPoolId != mPoolId) {
+                return;
+            }
             // Clear any temporary state variables
             clear();
 
@@ -85,17 +113,6 @@
         recycle();
     }
 
-    private final Context mContext;
-    private final int mFillResIconDpi;
-    private final int mIconBitmapSize;
-
-    private LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize) {
-        super(context, fillResIconDpi, iconBitmapSize);
-        mContext = context.getApplicationContext();
-        mFillResIconDpi = fillResIconDpi;
-        mIconBitmapSize = iconBitmapSize;
-    }
-
     public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
             int iconAppTargetSdk) {
         return createBadgedIconBitmap(icon, user, iconAppTargetSdk, false);
diff --git a/src/com/android/launcher3/popup/RemoteActionShortcut.java b/src/com/android/launcher3/popup/RemoteActionShortcut.java
index af0d3da..c76fb96 100644
--- a/src/com/android/launcher3/popup/RemoteActionShortcut.java
+++ b/src/com/android/launcher3/popup/RemoteActionShortcut.java
@@ -18,6 +18,7 @@
 
 import android.app.PendingIntent;
 import android.app.RemoteAction;
+import android.content.Intent;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.Log;
@@ -48,14 +49,20 @@
             AbstractFloatingView.closeAllOpenViews(launcher);
 
             try {
-                mAction.getActionIntent().send(0,
+                mAction.getActionIntent().send(
+                        launcher,
+                        0,
+                        new Intent().putExtra(
+                                Intent.EXTRA_PACKAGE_NAME,
+                                itemInfo.getTargetComponent().getPackageName()),
                         (pendingIntent, intent, resultCode, resultData, resultExtras) -> {
                             if (resultData != null && !resultData.isEmpty()) {
                                 Log.e(TAG, "Remote action returned result: " + mAction.getTitle()
                                         + " : " + resultData);
                                 Toast.makeText(launcher, resultData, Toast.LENGTH_SHORT).show();
                             }
-                        }, new Handler(Looper.getMainLooper()));
+                        },
+                        new Handler(Looper.getMainLooper()));
             } catch (PendingIntent.CanceledException e) {
                 Log.e(TAG, "Remote action canceled: " + mAction.getTitle(), e);
                 Toast.makeText(launcher, launcher.getString(
diff --git a/src/com/android/launcher3/util/ConfigMonitor.java b/src/com/android/launcher3/util/ConfigMonitor.java
index 5dd0d08..717acdc 100644
--- a/src/com/android/launcher3/util/ConfigMonitor.java
+++ b/src/com/android/launcher3/util/ConfigMonitor.java
@@ -29,9 +29,12 @@
 import android.view.Display;
 import android.view.WindowManager;
 
+import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.Utilities.Consumer;
+
 /**
  * {@link BroadcastReceiver} which watches configuration changes and
- * restarts the process in case changes which affect the device profile occur.
+ * notifies the callback in case changes which affect the device profile occur.
  */
 public class ConfigMonitor extends BroadcastReceiver implements DisplayListener {
 
@@ -48,7 +51,9 @@
     private final Point mRealSize;
     private final Point mSmallestSize, mLargestSize;
 
-    public ConfigMonitor(Context context) {
+    private Consumer<Context> mCallback;
+
+    public ConfigMonitor(Context context, Consumer<Context> callback) {
         mContext = context;
 
         Configuration config = context.getResources().getConfiguration();
@@ -64,6 +69,12 @@
         mSmallestSize = new Point();
         mLargestSize = new Point();
         display.getCurrentSizeRange(mSmallestSize, mLargestSize);
+
+        mCallback = callback;
+
+        mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
+        mContext.getSystemService(DisplayManager.class)
+                .registerDisplayListener(this, new Handler(UiThreadHelper.getBackgroundLooper()));
     }
 
     @Override
@@ -71,16 +82,10 @@
         Configuration config = context.getResources().getConfiguration();
         if (mFontScale != config.fontScale || mDensity != config.densityDpi) {
             Log.d(TAG, "Configuration changed");
-            killProcess();
+            notifyChange();
         }
     }
 
-    public void register() {
-        mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
-        mContext.getSystemService(DisplayManager.class)
-                .registerDisplayListener(this, new Handler(UiThreadHelper.getBackgroundLooper()));
-    }
-
     @Override
     public void onDisplayAdded(int displayId) { }
 
@@ -97,7 +102,7 @@
 
         if (!mRealSize.equals(mTmpPoint1) && !mRealSize.equals(mTmpPoint1.y, mTmpPoint1.x)) {
             Log.d(TAG, String.format("Display size changed from %s to %s", mRealSize, mTmpPoint1));
-            killProcess();
+            notifyChange();
             return;
         }
 
@@ -105,22 +110,28 @@
         if (!mSmallestSize.equals(mTmpPoint1) || !mLargestSize.equals(mTmpPoint2)) {
             Log.d(TAG, String.format("Available size changed from [%s, %s] to [%s, %s]",
                     mSmallestSize, mLargestSize, mTmpPoint1, mTmpPoint2));
-            killProcess();
+            notifyChange();
         }
     }
 
-    private void killProcess() {
-        Log.d(TAG, "restarting launcher");
-        try {
-            mContext.unregisterReceiver(this);
-            mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
-        } catch (Exception e) {
-            // We are going to die anyway, ignore any error die to race condition in registering.
+    private synchronized void notifyChange() {
+        if (mCallback != null) {
+            Consumer<Context> callback = mCallback;
+            mCallback = null;
+            new MainThreadExecutor().execute(() -> callback.accept(mContext));
         }
-        android.os.Process.killProcess(android.os.Process.myPid());
     }
 
     private Display getDefaultDisplay(Context context) {
         return context.getSystemService(WindowManager.class).getDefaultDisplay();
     }
+
+    public void unregister() {
+        try {
+            mContext.unregisterReceiver(this);
+            mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to unregister config monitor", e);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/util/SQLiteCacheHelper.java b/src/com/android/launcher3/util/SQLiteCacheHelper.java
index 44c1762..3faf070 100644
--- a/src/com/android/launcher3/util/SQLiteCacheHelper.java
+++ b/src/com/android/launcher3/util/SQLiteCacheHelper.java
@@ -87,6 +87,10 @@
         mOpenHelper.clearDB(mOpenHelper.getWritableDatabase());
     }
 
+    public void close() {
+        mOpenHelper.close();
+    }
+
     protected abstract void onCreateTable(SQLiteDatabase db);
 
     /**
diff --git a/tests/Android.mk b/tests/Android.mk
index d808873..a787537 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -21,7 +21,8 @@
 include $(CLEAR_VARS)
 LOCAL_STATIC_JAVA_LIBRARIES := \
 	androidx.annotation_annotation \
-	androidx-test \
+	androidx.test.runner \
+	androidx.test.rules \
 	androidx.test.uiautomator_uiautomator \
 	libSharedSystemUI
 
@@ -42,7 +43,8 @@
 
 LOCAL_MODULE_TAGS := tests
 LOCAL_STATIC_JAVA_LIBRARIES := \
-	androidx-test \
+	androidx.test.runner \
+	androidx.test.rules \
 	androidx.test.uiautomator_uiautomator \
 	mockito-target-minus-junit4