Merge "Make createIconBitmap public method Bug: 122545624" into ub-launcher3-master
diff --git a/Android.mk b/Android.mk
index 28f0bc2..cccf4e4 100644
--- a/Android.mk
+++ b/Android.mk
@@ -187,10 +187,13 @@
 LOCAL_SRC_FILES := \
     $(call all-java-files-under, src) \
     $(call all-java-files-under, quickstep/src) \
+    $(call all-java-files-under, quickstep/recents_ui_overrides/src) \
     $(call all-java-files-under, src_flags) \
     $(call all-java-files-under, src_shortcuts_overrides)
 
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/quickstep/res
+LOCAL_RESOURCE_DIR := \
+    $(LOCAL_PATH)/quickstep/res \
+    $(LOCAL_PATH)/quickstep/recents_ui_overrides/res
 LOCAL_PROGUARD_ENABLED := disabled
 
 
@@ -217,7 +220,9 @@
 LOCAL_PRIVILEGED_MODULE := true
 LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3
 
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/quickstep/res
+LOCAL_RESOURCE_DIR := \
+    $(LOCAL_PATH)/quickstep/res \
+    $(LOCAL_PATH)/quickstep/recents_ui_overrides/res
 
 LOCAL_FULL_LIBS_MANIFEST_FILES := \
     $(LOCAL_PATH)/AndroidManifest.xml \
@@ -249,10 +254,12 @@
 LOCAL_SRC_FILES := \
     $(call all-java-files-under, src) \
     $(call all-java-files-under, quickstep/src) \
+    $(call all-java-files-under, quickstep/recents_ui_overrides/src) \
     $(call all-java-files-under, go/src)
 
 LOCAL_RESOURCE_DIR := \
     $(LOCAL_PATH)/quickstep/res \
+    $(LOCAL_PATH)/quickstep/recents_ui_overrides/res \
     $(LOCAL_PATH)/go/res
 
 LOCAL_PROGUARD_FLAG_FILES := proguard.flags
@@ -271,6 +278,51 @@
 LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.launcher3.*
 include $(BUILD_PACKAGE)
 
+#
+# Build rule for Launcher3 Go app with quickstep and Go-specific
+# version of recents for Android Go devices.
+#
+include $(CLEAR_VARS)
+LOCAL_USE_AAPT2 := true
+LOCAL_MODULE_TAGS := optional
+
+ifneq (,$(wildcard frameworks/base))
+  LOCAL_STATIC_JAVA_LIBRARIES := SystemUISharedLib launcherprotosnano
+  LOCAL_PRIVATE_PLATFORM_APIS := true
+else
+  LOCAL_STATIC_JAVA_LIBRARIES := libSharedSystemUI libLauncherProtos
+  LOCAL_SDK_VERSION := system_current
+  LOCAL_MIN_SDK_VERSION := 26
+endif
+LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3CommonDepsLib
+
+LOCAL_SRC_FILES := \
+    $(call all-java-files-under, src) \
+    $(call all-java-files-under, quickstep/src) \
+    $(call all-java-files-under, go/src) \
+    $(call all-java-files-under, go/quickstep/src)
+
+LOCAL_RESOURCE_DIR := \
+    $(LOCAL_PATH)/quickstep/res \
+    $(LOCAL_PATH)/go/res \
+    $(LOCAL_PATH)/go/quickstep/res
+
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+LOCAL_PROGUARD_ENABLED := full
+
+LOCAL_PACKAGE_NAME := Launcher3QuickStepGoIconRecents
+LOCAL_PRIVILEGED_MODULE := true
+LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep
+
+LOCAL_FULL_LIBS_MANIFEST_FILES := \
+    $(LOCAL_PATH)/go/AndroidManifest.xml \
+    $(LOCAL_PATH)/AndroidManifest.xml \
+    $(LOCAL_PATH)/AndroidManifest-common.xml
+
+LOCAL_MANIFEST_FILE := quickstep/AndroidManifest.xml
+LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.launcher3.*
+include $(BUILD_PACKAGE)
+
 
 # ==================================================
 include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/build.gradle b/build.gradle
index 33409c5..4191d47 100644
--- a/build.gradle
+++ b/build.gradle
@@ -36,41 +36,56 @@
         targetCompatibility JavaVersion.VERSION_1_8
     }
 
-    flavorDimensions "default"
+    // The flavor dimensions for build variants (e.g. aospWithQuickstep, aospWithoutQuickstep)
+    // See: https://developer.android.com/studio/build/build-variants#flavor-dimensions
+    flavorDimensions "app", "recents"
 
     productFlavors {
         aosp {
-            dimension "default"
+            dimension "app"
             applicationId 'com.android.launcher3'
             testApplicationId 'com.android.launcher3.tests'
         }
 
         l3go {
-            dimension "default"
+            dimension "app"
             applicationId 'com.android.launcher3'
             testApplicationId 'com.android.launcher3.tests'
         }
 
-        quickstep {
-            dimension "default"
-            applicationId 'com.android.launcher3'
-            testApplicationId 'com.android.launcher3.tests'
+        withQuickstep {
+            dimension "recents"
 
             minSdkVersion 28
         }
+
+        withQuickstepIconRecents {
+            dimension "recents"
+
+            minSdkVersion 28
+        }
+
+        withoutQuickstep {
+            dimension "recents"
+        }
     }
 
     // Disable release builds for now
     android.variantFilter { variant ->
         if (variant.buildType.name.endsWith('release')) {
-            variant.setIgnore(true);
+            variant.setIgnore(true)
+        }
+
+        // Icon recents is Go only
+        if (name.contains("WithQuickstepIconRecents") && !name.contains("l3go")) {
+            variant.setIgnore(true)
         }
     }
 
     sourceSets {
         main {
             res.srcDirs = ['res']
-            java.srcDirs = ['src', 'src_shortcuts_overrides']
+            java.srcDirs = ['src', 'src_plugins']
             manifest.srcFile 'AndroidManifest-common.xml'
             proto {
                 srcDir 'protos/'
@@ -93,18 +108,29 @@
         }
 
         aosp {
-            java.srcDirs = ['src_flags', "src_ui_overrides"]
+            java.srcDirs = ['src_flags', 'src_shortcuts_overrides']
+            manifest.srcFile "AndroidManifest.xml"
         }
 
         l3go {
             res.srcDirs = ['go/res']
-            java.srcDirs = ['go/src', "src_ui_overrides"]
+            java.srcDirs = ['go/src']
             manifest.srcFile "go/AndroidManifest.xml"
         }
 
-        quickstep {
-            res.srcDirs = ['quickstep/res']
-            java.srcDirs = ['src_flags', 'quickstep/src']
+        withoutQuickstep {
+            java.srcDirs = ['src_ui_overrides']
+        }
+
+        withQuickstep {
+            res.srcDirs = ['quickstep/res', 'quickstep/recents_ui_overrides/res']
+            java.srcDirs = ['quickstep/src', 'quickstep/recents_ui_overrides/src']
+            manifest.srcFile "quickstep/AndroidManifest.xml"
+        }
+
+        withQuickstepIconRecents {
+            res.srcDirs = ['quickstep/res', 'go/quickstep/res']
+            java.srcDirs = ['quickstep/src', 'go/quickstep/src']
             manifest.srcFile "quickstep/AndroidManifest.xml"
         }
     }
@@ -121,14 +147,17 @@
     implementation "androidx.dynamicanimation:dynamicanimation:${ANDROID_X_VERSION}"
     implementation "androidx.recyclerview:recyclerview:${ANDROID_X_VERSION}"
     implementation "androidx.preference:preference:${ANDROID_X_VERSION}"
-    implementation PROTOBUF_DEPENDENCY
     implementation project(':IconLoader')
+    implementation fileTree(dir: "libs", include: 'launcher_protos.jar')
 
-    // This is already included in sysui_shared
-    aospImplementation fileTree(dir: "libs", include: 'plugin_core.jar')
-    l3goImplementation fileTree(dir: "libs", include: 'plugin_core.jar')
+    // Recents lib dependency
+    withQuickstepImplementation fileTree(dir: "quickstep/libs", include: 'sysui_shared.jar')
 
-    quickstepImplementation fileTree(dir: "quickstep/libs", include: 'sysui_shared.jar')
+    // Recents lib dependency for Go
+    withQuickstepIconRecentsImplementation fileTree(dir: "quickstep/libs", include: 'sysui_shared.jar')
+
+    // Required for AOSP to compile. This is already included in the sysui_shared.jar
+    withoutQuickstepImplementation fileTree(dir: "libs", include: 'plugin_core.jar')
 
     testImplementation 'junit:junit:4.12'
     androidTestImplementation "org.mockito:mockito-core:1.9.5"
diff --git a/go/quickstep/res/.keep b/go/quickstep/res/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/go/quickstep/res/.keep
diff --git a/go/quickstep/src/.keep b/go/quickstep/src/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/go/quickstep/src/.keep
diff --git a/quickstep/recents_ui_overrides/res/.keep b/quickstep/recents_ui_overrides/res/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/.keep
diff --git a/quickstep/recents_ui_overrides/src/.keep b/quickstep/recents_ui_overrides/src/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/.keep
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
index 753f73a..bef3e54 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
@@ -49,7 +49,7 @@
     private static final String TAG = "OverviewSwipeController";
 
     // Progress after which the transition is assumed to be a success in case user does not fling
-    private static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
+    public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
 
     protected final T mActivity;
     private final SwipeDetector mDetector;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
index 4e79fed..0ef67c4 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -44,6 +44,8 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.TouchController;
+import com.android.launcher3.util.UiThreadHelper;
+import com.android.launcher3.util.UiThreadHelper.AsyncCommand;
 import com.android.quickstep.OverviewInteractionState;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.util.RemoteFadeOutAnimationListener;
@@ -58,6 +60,9 @@
 
 public class UiFactory {
 
+    private static final AsyncCommand SET_SHELF_HEIGHT_CMD = (visible, height) ->
+            WindowManagerWrapper.getInstance().setShelfHeight(visible != 0, height);
+
     public static TouchController[] createTouchControllers(Launcher launcher) {
         boolean swipeUpEnabled = OverviewInteractionState.INSTANCE.get(launcher)
                 .isSwipeUpGestureEnabled();
@@ -175,10 +180,10 @@
         LauncherState state = launcher.getStateManager().getState();
         if (!OverviewInteractionState.INSTANCE.get(launcher).swipeGestureInitializing()) {
             DeviceProfile profile = launcher.getDeviceProfile();
-            WindowManagerWrapper.getInstance().setShelfHeight(
-                    (state == NORMAL || state == OVERVIEW) && launcher.isUserActive()
-                            && !profile.isVerticalBarLayout(),
-                    profile.hotseatBarSizePx);
+            boolean visible = (state == NORMAL || state == OVERVIEW) && launcher.isUserActive()
+                    && !profile.isVerticalBarLayout();
+            UiThreadHelper.runAsyncCommand(launcher, SET_SHELF_HEIGHT_CMD,
+                    visible ? 1 : 0, profile.hotseatBarSizePx);
         }
 
         if (state == NORMAL) {
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index 4646fd7f..1402283 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -44,6 +44,8 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.view.View;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
@@ -77,9 +79,6 @@
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
 /**
  * Utility class which abstracts out the logical differences between Launcher and RecentsActivity.
  */
diff --git a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
index 95be188..e27af2a 100644
--- a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
+++ b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
@@ -21,11 +21,9 @@
 import static android.view.MotionEvent.ACTION_POINTER_UP;
 import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.INVALID_POINTER_ID;
-
 import static com.android.launcher3.util.RaceConditionTracker.ENTER;
 import static com.android.launcher3.util.RaceConditionTracker.EXIT;
-import static com.android.systemui.shared.system.ActivityManagerWrapper
-        .CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
 
 import android.annotation.TargetApi;
@@ -192,7 +190,7 @@
 
                 if (mPassedInitialSlop && mInteractionHandler != null) {
                     // Move
-                    mInteractionHandler.updateDisplacement(displacement - mStartDisplacement);
+                    dispatchMotion(ev, displacement - mStartDisplacement);
                 }
                 break;
             }
@@ -207,6 +205,14 @@
         }
     }
 
+    private void dispatchMotion(MotionEvent ev, float displacement) {
+        mInteractionHandler.updateDisplacement(displacement);
+        boolean isLandscape = isNavBarOnLeft() || isNavBarOnRight();
+        if (!isLandscape) {
+            mInteractionHandler.dispatchMotionEventToRecentsView(ev);
+        }
+    }
+
     private void notifyGestureStarted() {
         if (mInteractionHandler == null) {
             return;
@@ -297,15 +303,16 @@
      */
     private void finishTouchTracking(MotionEvent ev) {
         if (mPassedInitialSlop && mInteractionHandler != null) {
-            mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement);
+            dispatchMotion(ev, getDisplacement(ev) - mStartDisplacement);
 
             mVelocityTracker.computeCurrentVelocity(1000,
                     ViewConfiguration.get(this).getScaledMaximumFlingVelocity());
 
-            float velocity = isNavBarOnRight() ? mVelocityTracker.getXVelocity(mActivePointerId)
-                    : isNavBarOnLeft() ? -mVelocityTracker.getXVelocity(mActivePointerId)
+            float velocityX = mVelocityTracker.getXVelocity(mActivePointerId);
+            float velocity = isNavBarOnRight() ? velocityX
+                    : isNavBarOnLeft() ? -velocityX
                             : mVelocityTracker.getYVelocity(mActivePointerId);
-            mInteractionHandler.onGestureEnded(velocity);
+            mInteractionHandler.onGestureEnded(velocity, velocityX);
         } else {
             // Since we start touch tracking on DOWN, we may reach this state without actually
             // starting the gesture. In that case, just cleanup immediately.
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java b/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java
index 2f3cb5f..042afea 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java
@@ -97,8 +97,8 @@
     }
 
     /**
-     * @param onFinishComplete A callback that runs after the animation controller has finished
-     *                         on the background thread.
+     * @param onFinishComplete A callback that runs on the main thread after the animation
+     *                         controller has finished on the background thread.
      */
     public void finish(boolean toHome, Runnable onFinishComplete) {
         if (!toHome) {
@@ -128,7 +128,7 @@
             controller.finish(toHome);
 
             if (onFinishComplete != null) {
-                onFinishComplete.run();
+                mMainThreadExecutor.execute(onFinishComplete);
             }
         }
     }
diff --git a/quickstep/src/com/android/quickstep/TaskListStabilizer.java b/quickstep/src/com/android/quickstep/TaskListStabilizer.java
index 0d23973..3eb26b4 100644
--- a/quickstep/src/com/android/quickstep/TaskListStabilizer.java
+++ b/quickstep/src/com/android/quickstep/TaskListStabilizer.java
@@ -17,79 +17,153 @@
 
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASK_STABILIZER;
 
+import android.app.ActivityManager.RecentTaskInfo;
+import android.content.ComponentName;
+import android.os.Process;
 import android.os.SystemClock;
-import android.util.SparseArray;
+import android.util.Log;
 
 import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.IntSet;
 import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.TaskStackChangeListener;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 public class TaskListStabilizer {
 
     private static final long TASK_CACHE_TIMEOUT_MS = 5000;
 
-    private final SparseArray<Task> mTempMap = new SparseArray<>();
-    private final IntArray mTempArray = new IntArray();
-    private final IntSet mTempSet = new IntSet();
+    private static final int INVALID_TASK_ID = -1;
 
-    private final IntArray mLastStableOrder = new IntArray();
-    private final IntSet mLastSet = new IntSet();
-    private final IntArray mLastUnstableOrder = new IntArray();
+    private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
 
-    private long mLastReorderTime;
+        @Override
+        public void onTaskCreated(int taskId, ComponentName componentName) {
+            onTaskCreatedInternal(taskId);
+        }
 
-    public ArrayList<Task> reorder(ArrayList<Task> tasks) {
+        @Override
+        public void onTaskMovedToFront(int taskId) {
+            onTaskMovedToFrontInternal(taskId);
+        }
+
+        @Override
+        public void onTaskRemoved(int taskId) {
+            onTaskRemovedInternal(taskId);
+        }
+    };
+
+    // Task ids ordered based on recency, 0th index is the latest task
+    private final IntArray mOrderedTaskIds;
+
+    // Wrapper objects used for sorting tasks
+    private final ArrayList<TaskWrapper> mTaskWrappers = new ArrayList<>();
+
+    // Information about recent task re-order which has not been applied yet
+    private int mScheduledMoveTaskId = INVALID_TASK_ID;
+    private long mScheduledMoveTime = 0;
+
+    public TaskListStabilizer() {
+        if (ENABLE_TASK_STABILIZER.get()) {
+            // Initialize the task ids map
+            List<RecentTaskInfo> rawTasks = ActivityManagerWrapper.getInstance().getRecentTasks(
+                    Integer.MAX_VALUE, Process.myUserHandle().getIdentifier());
+            mOrderedTaskIds = new IntArray(rawTasks.size());
+            for (RecentTaskInfo info : rawTasks) {
+                mOrderedTaskIds.add(new TaskKey(info).id);
+            }
+
+            ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
+        } else {
+            mOrderedTaskIds = null;
+        }
+    }
+
+    private synchronized void onTaskCreatedInternal(int taskId) {
+        applyScheduledMoveUnchecked();
+        mOrderedTaskIds.add(taskId);
+    }
+
+    private synchronized void onTaskRemovedInternal(int taskId) {
+        applyScheduledMoveUnchecked();
+        mOrderedTaskIds.removeValue(taskId);
+    }
+
+    private void applyScheduledMoveUnchecked() {
+        if (mScheduledMoveTaskId != INVALID_TASK_ID) {
+            // Mode the scheduled task to front
+            mOrderedTaskIds.removeValue(mScheduledMoveTaskId);
+            mOrderedTaskIds.add(mScheduledMoveTaskId);
+            mScheduledMoveTaskId = INVALID_TASK_ID;
+        }
+    }
+
+    /**
+     * Checks if the scheduled move has timed out and moves the task to front accordingly.
+     */
+    private void applyScheduledMoveIfTime() {
+        if (mScheduledMoveTaskId != INVALID_TASK_ID
+                && (SystemClock.uptimeMillis() - mScheduledMoveTime) > TASK_CACHE_TIMEOUT_MS) {
+            applyScheduledMoveUnchecked();
+        }
+    }
+
+    private synchronized void onTaskMovedToFrontInternal(int taskId) {
+        applyScheduledMoveIfTime();
+        mScheduledMoveTaskId = taskId;
+        mScheduledMoveTime = SystemClock.uptimeMillis();
+    }
+
+
+    public synchronized ArrayList<Task> reorder(ArrayList<Task> tasks) {
         if (!ENABLE_TASK_STABILIZER.get()) {
             return tasks;
         }
 
-        // Create task id array
-        int count = tasks.size();
-        mTempArray.clear();
-        mTempSet.clear();
-        mTempMap.clear();
+        applyScheduledMoveIfTime();
 
-        for (int i = 0; i < count; i++) {
-            Task t = tasks.get(i);
-            mTempMap.put(t.key.id, t);
-
-            mTempSet.add(t.key.id);
-            mTempArray.add(t.key.id);
+        // Ensure that we have enough wrappers
+        int taskCount = tasks.size();
+        for (int i = taskCount - mTaskWrappers.size(); i > 0; i--) {
+            mTaskWrappers.add(new TaskWrapper());
         }
 
-        if (mLastSet.equals(mTempSet) && isStabilizationQuickEnough()) {
-            if (mLastStableOrder.equals(mTempArray)) {
-                // Everything is same
-                return tasks;
-            }
+        List<TaskWrapper> listToSort = mTaskWrappers.size() == taskCount
+                ? mTaskWrappers : mTaskWrappers.subList(0, taskCount);
+        int missingTaskIndex = -taskCount;
 
-            if (!mLastUnstableOrder.equals(mTempArray)) {
-                // Fast reordering, record the current time.
-                mLastUnstableOrder.copyFrom(mTempArray);
-                mLastReorderTime = SystemClock.uptimeMillis();
-            }
+        for (int i = 0; i < taskCount; i++){
+            TaskWrapper wrapper = listToSort.get(i);
+            wrapper.task = tasks.get(i);
+            wrapper.index = mOrderedTaskIds.indexOf(wrapper.task.key.id);
 
-            // Reorder the tasks based on the last stable order.
-            ArrayList<Task> sorted = new ArrayList<>(count);
-            for (int i = 0; i < count; i++) {
-                sorted.add(mTempMap.get(mLastStableOrder.get(i)));
+            // Ensure that missing tasks are put in the front, in the order they appear in the
+            // original list
+            if (wrapper.index < 0) {
+                wrapper.index = missingTaskIndex;
+                missingTaskIndex++;
             }
-            return sorted;
         }
+        Collections.sort(listToSort);
 
-        // Cache the data
-        mLastStableOrder.copyFrom(mTempArray);
-        mLastUnstableOrder.copyFrom(mTempArray);
-        mLastSet.copyFrom(mTempSet);
-
-        mLastReorderTime = SystemClock.uptimeMillis();
-
-        return tasks;
+        ArrayList<Task> result = new ArrayList<>(taskCount);
+        for (int i = 0; i < taskCount; i++) {
+            result.add(listToSort.get(i).task);
+        }
+        return result;
     }
 
-    private boolean isStabilizationQuickEnough() {
-        return (SystemClock.uptimeMillis() - mLastReorderTime) < TASK_CACHE_TIMEOUT_MS;
+    private static class TaskWrapper implements Comparable<TaskWrapper> {
+        Task task;
+        int index;
+
+        @Override
+        public int compareTo(TaskWrapper other) {
+            return Integer.compare(index, other.index);
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index e951750..ac21517 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -46,10 +46,12 @@
 import android.os.UserHandle;
 import android.util.Log;
 import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewTreeObserver.OnDrawListener;
 import android.view.WindowManager;
 import android.view.animation.Interpolator;
+
 import androidx.annotation.AnyThread;
 import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
@@ -109,7 +111,7 @@
 
     // Interaction finish states
     private static final int STATE_SCALED_CONTROLLER_RECENTS = 1 << 5;
-    private static final int STATE_SCALED_CONTROLLER_APP = 1 << 6;
+    private static final int STATE_SCALED_CONTROLLER_LAST_TASK = 1 << 6;
 
     private static final int STATE_HANDLER_INVALIDATED = 1 << 7;
     private static final int STATE_GESTURE_STARTED_QUICKSTEP = 1 << 8;
@@ -127,7 +129,8 @@
     private static final int STATE_SCREENSHOT_VIEW_SHOWN = 1 << 17;
 
     private static final int STATE_RESUME_LAST_TASK = 1 << 18;
-    private static final int STATE_ASSIST_DATA_RECEIVED = 1 << 19;
+    private static final int STATE_START_NEW_TASK = 1 << 19;
+    private static final int STATE_ASSIST_DATA_RECEIVED = 1 << 20;
 
 
     private static final int LAUNCHER_UI_STATES =
@@ -153,7 +156,7 @@
             "STATE_ACTIVITY_MULTIPLIER_COMPLETE",
             "STATE_APP_CONTROLLER_RECEIVED",
             "STATE_SCALED_CONTROLLER_RECENTS",
-            "STATE_SCALED_CONTROLLER_APP",
+            "STATE_SCALED_CONTROLLER_LAST_TASK",
             "STATE_HANDLER_INVALIDATED",
             "STATE_GESTURE_STARTED_QUICKSTEP",
             "STATE_GESTURE_STARTED_QUICKSCRUB",
@@ -166,6 +169,7 @@
             "STATE_SCREENSHOT_CAPTURED",
             "STATE_SCREENSHOT_VIEW_SHOWN",
             "STATE_RESUME_LAST_TASK",
+            "STATE_START_NEW_TASK",
             "STATE_ASSIST_DATA_RECEIVED",
     };
 
@@ -173,7 +177,7 @@
     public static final long MIN_SWIPE_DURATION = 80;
     public static final long MIN_OVERSHOOT_DURATION = 120;
 
-    public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
+    public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.7f;
     private static final float SWIPE_DURATION_MULTIPLIER =
             Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW));
 
@@ -190,6 +194,7 @@
     // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
     // visible.
     private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
+    private boolean mDispatchedDownEvent;
     // To avoid UI jump when gesture is started, we offset the animation by the threshold.
     private float mShiftAtGestureStart = 0;
 
@@ -298,10 +303,12 @@
         mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED,
                 this::sendRemoteAnimationsToAnimationFactory);
 
-        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_APP,
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_LAST_TASK,
                 this::resumeLastTaskForQuickstep);
         mStateCallback.addCallback(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED,
                 this::resumeLastTask);
+        mStateCallback.addCallback(STATE_START_NEW_TASK | STATE_APP_CONTROLLER_RECEIVED,
+                this::startNewTask);
 
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
                         | STATE_ACTIVITY_MULTIPLIER_COMPLETE
@@ -327,7 +334,7 @@
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
                 this::invalidateHandlerWithLauncher);
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED
-                | STATE_SCALED_CONTROLLER_APP,
+                | STATE_SCALED_CONTROLLER_LAST_TASK,
                 this::notifyTransitionCancelled);
 
         mStateCallback.addCallback(QUICK_SCRUB_START_UI_STATE, this::onQuickScrubStartUi);
@@ -410,6 +417,9 @@
         SyncRtSurfaceTransactionApplierCompat.create(mRecentsView, (applier) -> {
             mSyncTransactionApplier = applier;
         });
+        mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
+            updateFinalShift();
+        });
         mQuickScrubController = mRecentsView.getQuickScrubController();
         mLayoutListener = mActivityControlHelper.createLayoutListener(mActivity);
 
@@ -535,15 +545,39 @@
         } else {
             offsetX = res.getDimensionPixelSize(R.dimen.recents_page_spacing)
                     + tempRect.rect.width();
-            float distanceToReachEdge = mDp.widthPx / 2 + tempRect.rect.width() / 2 +
-                    res.getDimensionPixelSize(R.dimen.recents_page_spacing);
-            float interpolation = Math.min(1, offsetX / distanceToReachEdge);
-            scale = TaskView.getCurveScaleForInterpolation(interpolation);
+            scale = getTaskCurveScaleForOffsetX(offsetX, tempRect.rect.width());
         }
         mClipAnimationHelper.offsetTarget(scale, Utilities.isRtl(res) ? -offsetX : offsetX, offsetY,
                 QuickScrubController.QUICK_SCRUB_START_INTERPOLATOR);
     }
 
+    private float getTaskCurveScaleForOffsetX(float offsetX, float taskWidth) {
+        float distanceToReachEdge = mDp.widthPx / 2 + taskWidth / 2 +
+                mContext.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
+        float interpolation = Math.min(1, offsetX / distanceToReachEdge);
+        return TaskView.getCurveScaleForInterpolation(interpolation);
+    }
+
+    @WorkerThread
+    public void dispatchMotionEventToRecentsView(MotionEvent event) {
+        if (mRecentsView == null) {
+            return;
+        }
+        // Pass the motion events to RecentsView to allow scrolling during swipe up.
+        if (mDispatchedDownEvent) {
+            mRecentsView.dispatchTouchEvent(event);
+        } else {
+            // The first event we dispatch should be ACTION_DOWN.
+            mDispatchedDownEvent = true;
+            MotionEvent downEvent = MotionEvent.obtain(event);
+            downEvent.setAction(MotionEvent.ACTION_DOWN);
+            int flags = downEvent.getEdgeFlags();
+            downEvent.setEdgeFlags(flags | TouchInteractionService.EDGE_NAV_BAR);
+            mRecentsView.dispatchTouchEvent(downEvent);
+            downEvent.recycle();
+        }
+    }
+
     @WorkerThread
     public void updateDisplacement(float displacement) {
         // We are moving in the negative x/y direction
@@ -588,11 +622,21 @@
 
         RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController();
         if (controller != null) {
+            float offsetX = 0;
+            if (mRecentsView != null && mInteractionType == INTERACTION_NORMAL) {
+                int startScroll = mRecentsView.getScrollForPage(mRecentsView.indexOfChild(
+                        mRecentsView.getRunningTaskView()));
+                offsetX = startScroll - mRecentsView.getScrollX();
+                offsetX *= mRecentsView.getScaleX();
+            }
+            float offsetScale = getTaskCurveScaleForOffsetX(offsetX,
+                    mClipAnimationHelper.getTargetRect().width());
             SyncRtSurfaceTransactionApplierCompat syncTransactionApplier
                     = Looper.myLooper() == mMainThreadHandler.getLooper()
                             ? mSyncTransactionApplier
                             : null;
-            mTransformParams.setProgress(shift).setSyncTransactionApplier(syncTransactionApplier);
+            mTransformParams.setProgress(shift).setOffsetX(offsetX).setOffsetScale(offsetScale)
+                    .setSyncTransactionApplier(syncTransactionApplier);
             mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet,
                     mTransformParams);
 
@@ -621,10 +665,17 @@
                     HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
             }
         }
-        // Update insets of the next previous task, as we might switch to it.
-        TaskView nextTaskView = mRecentsView == null ? null : mRecentsView.getNextTaskView();
-        if (mInteractionType == INTERACTION_NORMAL && nextTaskView != null) {
-            nextTaskView.setFullscreenProgress(1 - mCurrentShift.value);
+        // Update insets of the adjacent tasks, as we might switch to them.
+        int runningTaskIndex = mRecentsView == null ? -1 : mRecentsView.getRunningTaskIndex();
+        if (mInteractionType == INTERACTION_NORMAL && runningTaskIndex >= 0) {
+            TaskView nextTaskView = mRecentsView.getTaskViewAt(runningTaskIndex + 1);
+            TaskView prevTaskView = mRecentsView.getTaskViewAt(runningTaskIndex - 1);
+            if (nextTaskView != null) {
+                nextTaskView.setFullscreenProgress(1 - mCurrentShift.value);
+            }
+            if (prevTaskView != null) {
+                prevTaskView.setFullscreenProgress(1 - mCurrentShift.value);
+            }
         }
 
         if (mLauncherTransitionController == null || mLauncherTransitionController
@@ -714,7 +765,7 @@
     }
 
     @WorkerThread
-    public void onGestureEnded(float endVelocity) {
+    public void onGestureEnded(float endVelocity, float velocityX) {
         float flingThreshold = mContext.getResources()
                 .getDimension(R.dimen.quickstep_fling_threshold_velocity);
         boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold;
@@ -723,9 +774,9 @@
         mLogAction = isFling ? Touch.FLING : Touch.SWIPE;
 
         if (mBgLongSwipeMode) {
-            executeOnUiThread(() -> onLongSwipeGestureFinishUi(endVelocity, isFling));
+            executeOnUiThread(() -> onLongSwipeGestureFinishUi(endVelocity, isFling, velocityX));
         } else {
-            handleNormalGestureEnd(endVelocity, isFling);
+            handleNormalGestureEnd(endVelocity, isFling, velocityX);
         }
     }
 
@@ -742,16 +793,21 @@
                 mTouchInteractionLog);
     }
 
-    private void handleNormalGestureEnd(float endVelocity, boolean isFling) {
+    private void handleNormalGestureEnd(float endVelocity, boolean isFling, float velocityX) {
         float velocityPxPerMs = endVelocity / 1000;
+        float velocityXPxPerMs = velocityX / 1000;
         long duration = MAX_SWIPE_DURATION;
         float currentShift = mCurrentShift.value;
         final boolean goingToHome;
         float endShift;
         final float startShift;
         Interpolator interpolator = DEACCEL;
+        final int nextPage = mRecentsView != null ? mRecentsView.getNextPage() : -1;
+        final int runningTaskIndex = mRecentsView != null ? mRecentsView.getRunningTaskIndex() : -1;
+        boolean goingToNewTask = mRecentsView != null && nextPage != runningTaskIndex;
+        final boolean reachedOverviewThreshold = currentShift >= MIN_PROGRESS_FOR_OVERVIEW;
         if (!isFling) {
-            goingToHome = currentShift >= MIN_PROGRESS_FOR_OVERVIEW && mGestureStarted;
+            goingToHome = reachedOverviewThreshold && mGestureStarted;
             endShift = goingToHome ? 1 : 0;
             long expectedDuration = Math.abs(Math.round((endShift - currentShift)
                     * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
@@ -759,7 +815,9 @@
             startShift = currentShift;
             interpolator = goingToHome ? OVERSHOOT_1_2 : DEACCEL;
         } else {
-            goingToHome = endVelocity < 0;
+            // If user scrolled to a new task, only go to home (overview) if they already passed
+            // the overview threshold. Otherwise, we'll snap to the new task and launch it.
+            goingToHome = endVelocity < 0 && (!goingToNewTask || reachedOverviewThreshold);
             endShift = goingToHome ? 1 : 0;
             startShift = Utilities.boundToRange(currentShift - velocityPxPerMs
                     * SINGLE_FRAME_MS / mTransitionDragLength, 0, 1);
@@ -786,9 +844,29 @@
         }
         if (goingToHome) {
             mRecentsAnimationWrapper.enableTouchProxy();
+        } else if (goingToNewTask) {
+            // We aren't goingToHome, and user scrolled/flung to a new task; snap to the closest
+            // task in that direction and launch it (in startNewTask()).
+            int taskToLaunch = runningTaskIndex + (nextPage > runningTaskIndex ? 1 : - 1);
+            if (taskToLaunch >= mRecentsView.getTaskViewCount()) {
+                // Scrolled to Clear all button, snap back to current task and resume it.
+                mRecentsView.snapToPage(runningTaskIndex, Math.toIntExact(duration));
+                goingToNewTask = false;
+            } else {
+                float distance = Math.abs(mRecentsView.getScrollForPage(taskToLaunch)
+                        - mRecentsView.getScrollX());
+                int durationX = (int) Math.abs(distance / velocityXPxPerMs);
+                if (durationX > MAX_SWIPE_DURATION) {
+                    durationX = Math.toIntExact(MAX_SWIPE_DURATION);
+                }
+                interpolator = Interpolators.scrollInterpolatorForVelocity(velocityXPxPerMs);
+                mRecentsView.snapToPage(taskToLaunch, durationX, interpolator);
+                duration = Math.max(duration, durationX);
+            }
         }
 
-        animateToProgress(startShift, endShift, duration, interpolator, goingToHome);
+        animateToProgress(startShift, endShift, duration, interpolator, goingToHome,
+                goingToNewTask);
     }
 
     private void doLogGesture(boolean toLauncher) {
@@ -814,22 +892,26 @@
 
     /** Animates to the given progress, where 0 is the current app and 1 is overview. */
     private void animateToProgress(float start, float end, long duration,
-            Interpolator interpolator, boolean goingToHome) {
+            Interpolator interpolator, boolean goingToHome, boolean goingToNewTask) {
         mRecentsAnimationWrapper.runOnInit(() -> animateToProgressInternal(start, end, duration,
-                interpolator, goingToHome));
+                interpolator, goingToHome, goingToNewTask));
     }
 
     private void animateToProgressInternal(float start, float end, long duration,
-            Interpolator interpolator, boolean goingToHome) {
+            Interpolator interpolator, boolean goingToHome, boolean goingToNewTask) {
         mIsGoingToHome = goingToHome;
         ObjectAnimator anim = mCurrentShift.animateToValue(start, end).setDuration(duration);
         anim.setInterpolator(interpolator);
         anim.addListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationSuccess(Animator animator) {
+                int recentsState = STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
+                        | STATE_SCREENSHOT_VIEW_SHOWN;
                 setStateOnUiThread(mIsGoingToHome
-                        ? (STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
-                        | STATE_SCREENSHOT_VIEW_SHOWN) : STATE_SCALED_CONTROLLER_APP);
+                        ? recentsState
+                        : goingToNewTask
+                            ? STATE_START_NEW_TASK
+                            : STATE_SCALED_CONTROLLER_LAST_TASK);
             }
         });
         anim.start();
@@ -872,6 +954,18 @@
         mTouchInteractionLog.finishRecentsAnimation(false);
     }
 
+    @UiThread
+    private void startNewTask() {
+        // Launch the task user scrolled to (mRecentsView.getNextPage()).
+        mRecentsAnimationWrapper.finish(true /* toHome */, () -> {
+            mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).launchTask(false,
+                    result -> setStateOnUiThread(STATE_HANDLER_INVALIDATED),
+                    mMainThreadHandler);
+        });
+        mTouchInteractionLog.finishRecentsAnimation(false);
+        doLogGesture(false /* toLauncher */);
+    }
+
     public void reset() {
         if (mInteractionType != INTERACTION_QUICK_SCRUB) {
             // Only invalidate the handler if we are not quick scrubbing, otherwise, it will be
@@ -889,6 +983,10 @@
 
         mActivityInitListener.unregister();
         mTaskSnapshot = null;
+
+        if (mRecentsView != null) {
+            mRecentsView.setOnScrollChangeListener(null);
+        }
     }
 
     private void invalidateHandlerWithLauncher() {
@@ -999,7 +1097,8 @@
         long duration = FeatureFlags.QUICK_SWITCH.get()
                 ? QUICK_SWITCH_FROM_APP_START_DURATION
                 : QUICK_SCRUB_FROM_APP_START_DURATION;
-        animateToProgress(mCurrentShift.value, 1f, duration, LINEAR, true /* goingToHome */);
+        animateToProgress(mCurrentShift.value, 1f, duration, LINEAR, true /* goingToHome */,
+                false /* goingToNewTask */);
     }
 
     private void onQuickScrubStartUi() {
@@ -1152,10 +1251,10 @@
         setTargetAlphaProvider(WindowTransformSwipeHandler::getHiddenTargetAlpha);
     }
 
-    private void onLongSwipeGestureFinishUi(float velocity, boolean isFling) {
+    private void onLongSwipeGestureFinishUi(float velocity, boolean isFling, float velocityX) {
         if (!mUiLongSwipeMode || mLongSwipeController == null) {
             mUiLongSwipeMode = false;
-            handleNormalGestureEnd(velocity, isFling);
+            handleNormalGestureEnd(velocity, isFling, velocityX);
             return;
         }
         mUiLongSwipeMode = false;
diff --git a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
index 431517a..6a8482b 100644
--- a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
+++ b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
@@ -154,10 +154,11 @@
     public RectF applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params) {
         RectF currentRect;
         mTmpRectF.set(mTargetRect);
-        Utilities.scaleRectFAboutCenter(mTmpRectF, mTargetScale);
+        Utilities.scaleRectFAboutCenter(mTmpRectF, mTargetScale * params.offsetScale);
         float offsetYProgress = mOffsetYInterpolator.getInterpolation(params.progress);
         float progress = mInterpolator.getInterpolation(params.progress);
         currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF);
+        currentRect.offset(params.offsetX, 0);
 
         synchronized (mTargetOffset) {
             // Stay lined up with the center of the target, since it moves for quick scrub.
@@ -353,10 +354,14 @@
 
     public static class TransformParams {
         float progress;
+        float offsetX;
+        float offsetScale;
         SyncRtSurfaceTransactionApplierCompat syncTransactionApplier;
 
         public TransformParams() {
             progress = 0;
+            offsetX = 0;
+            offsetScale = 1;
         }
 
         public TransformParams setProgress(float progress) {
@@ -364,6 +369,16 @@
             return this;
         }
 
+        public TransformParams setOffsetX(float offsetX) {
+            this.offsetX = offsetX;
+            return this;
+        }
+
+        public TransformParams setOffsetScale(float offsetScale) {
+            this.offsetScale = offsetScale;
+            return this;
+        }
+
         public TransformParams setSyncTransactionApplier(
                 SyncRtSurfaceTransactionApplierCompat applier) {
             this.syncTransactionApplier = applier;
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 34b5748..e2a4dda 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -21,9 +21,11 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.uioverrides.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
-import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
+
+import static com.android.quickstep.TouchInteractionService.EDGE_NAV_BAR;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -60,6 +62,7 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.ListView;
+
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BaseActivity;
@@ -421,7 +424,8 @@
                         final boolean clearAllButtonDeadZoneConsumed =
                                 mClearAllButton.getAlpha() == 1
                                         && mClearAllButtonDeadZoneRect.contains(x, y);
-                        if (!clearAllButtonDeadZoneConsumed
+                        final boolean cameFromNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
+                        if (!clearAllButtonDeadZoneConsumed && !cameFromNavBar
                                 && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) {
                             mTouchDownToStartHome = true;
                         }
@@ -732,6 +736,11 @@
         return getTaskView(mRunningTaskId);
     }
 
+    public int getRunningTaskIndex() {
+        TaskView tv = getRunningTaskView();
+        return tv == null ? -1 : indexOfChild(tv);
+    }
+
     /**
      * Hides the tile associated with {@link #mRunningTaskId}
      */
@@ -756,8 +765,7 @@
         setRunningTaskIconScaledDown(runningTaskIconScaledDown);
         setRunningTaskHidden(runningTaskTileHidden);
 
-        TaskView tv = getRunningTaskView();
-        setCurrentPage(tv == null ? 0 : indexOfChild(tv));
+        setCurrentPage(getRunningTaskIndex());
 
         // Load the tasks (if the loading is already
         mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
@@ -1346,7 +1354,7 @@
                             : 0);
 
             // Passing the threshold from taskview to fullscreen app will vibrate
-            final boolean passed = animator.getAnimatedFraction() >= MIN_PROGRESS_FOR_OVERVIEW;
+            final boolean passed = animator.getAnimatedFraction() >= SUCCESS_TRANSITION_PROGRESS;
             if (passed != passedOverviewThreshold[0]) {
                 passedOverviewThreshold[0] = passed;
                 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index bb6f514..e1e596b 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -46,7 +46,7 @@
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.quickstep.RecentsModel;
@@ -119,7 +119,7 @@
             new FloatProperty<TaskView>("focusTransition") {
                 @Override
                 public void setValue(TaskView taskView, float v) {
-                    taskView.setIconAndDimTransitionProgress(v);
+                    taskView.setIconAndDimTransitionProgress(v, false /* invert */);
                 }
 
                 @Override
@@ -316,11 +316,17 @@
         }
     }
 
-    private void setIconAndDimTransitionProgress(float progress) {
+    private void setIconAndDimTransitionProgress(float progress, boolean invert) {
+        if (invert) {
+            progress = 1 - progress;
+        }
         mFocusTransitionProgress = progress;
         mSnapshotView.setDimAlphaMultipler(progress);
-        float scale = FAST_OUT_SLOW_IN.getInterpolation(Utilities.boundToRange(
-                progress * DIM_ANIM_DURATION / SCALE_ICON_DURATION, 0, 1));
+        float iconScalePercentage = (float) SCALE_ICON_DURATION / DIM_ANIM_DURATION;
+        float lowerClamp = invert ? 1f - iconScalePercentage : 0;
+        float upperClamp = invert ? 1 : iconScalePercentage;
+        float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, lowerClamp, upperClamp)
+                .getInterpolation(progress);
         mIconView.setScaleX(scale);
         mIconView.setScaleY(scale);
     }
@@ -341,10 +347,14 @@
     }
 
     protected void setIconScaleAndDim(float iconScale) {
+        setIconScaleAndDim(iconScale, false);
+    }
+
+    private void setIconScaleAndDim(float iconScale, boolean invert) {
         if (mIconAndDimAnimator != null) {
             mIconAndDimAnimator.cancel();
         }
-        setIconAndDimTransitionProgress(iconScale);
+        setIconAndDimTransitionProgress(iconScale, invert);
     }
 
     public void resetVisualProperties() {
@@ -524,7 +534,8 @@
         }
         mFullscreenProgress = progress;
         boolean isFullscreen = mFullscreenProgress > 0;
-        mIconView.setVisibility(isFullscreen ? INVISIBLE : VISIBLE);
+        setIconScaleAndDim(progress, true /* invert */);
+        mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE);
         setClipChildren(!isFullscreen);
         setClipToPadding(!isFullscreen);
         getThumbnail().invalidate();
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 8c4dd1e..53877ff 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -33,9 +33,11 @@
     <attr name="workspaceKeyShadowColor" format="color" />
     <attr name="workspaceStatusBarScrim" format="reference" />
     <attr name="widgetsTheme" format="reference" />
-    <attr name="folderDotColor" format="color" />
     <attr name="loadingIconColor" format="color" />
 
+    <attr name="folderDotColor" format="color" />
+    <attr name="folderIconRadius" format="float" />
+
     <!-- BubbleTextView specific attributes. -->
     <declare-styleable name="BubbleTextView">
         <attr name="layoutHorizontal" format="boolean" />
diff --git a/res/xml/folder_shapes.xml b/res/xml/folder_shapes.xml
new file mode 100644
index 0000000..e60d333
--- /dev/null
+++ b/res/xml/folder_shapes.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 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.
+-->
+<shapes xmlns:launcher="http://schemas.android.com/apk/res-auto" >
+
+    <Circle launcher:folderIconRadius="1" />
+
+    <!-- Default icon for AOSP -->
+    <RoundedSquare launcher:folderIconRadius="0.16" />
+
+    <!-- Rounded icon from RRO -->
+    <RoundedSquare launcher:folderIconRadius="0.6" />
+
+    <!-- Square icon -->
+    <RoundedSquare launcher:folderIconRadius="0" />
+
+    <TearDrop launcher:folderIconRadius="0.3" />
+    <Squircle launcher:folderIconRadius="0.2" />
+
+</shapes>
\ No newline at end of file
diff --git a/src/com/android/launcher3/MainProcessInitializer.java b/src/com/android/launcher3/MainProcessInitializer.java
index 9692d73..93df025 100644
--- a/src/com/android/launcher3/MainProcessInitializer.java
+++ b/src/com/android/launcher3/MainProcessInitializer.java
@@ -38,6 +38,6 @@
         FileLog.setDir(context.getApplicationContext().getFilesDir());
         FeatureFlags.initialize(context);
         SessionCommitReceiver.applyDefaultUserPrefs(context);
-        FolderShape.init();
+        FolderShape.init(context);
     }
 }
diff --git a/src/com/android/launcher3/folder/FolderShape.java b/src/com/android/launcher3/folder/FolderShape.java
index ae279cb..f7cdb77 100644
--- a/src/com/android/launcher3/folder/FolderShape.java
+++ b/src/com/android/launcher3/folder/FolderShape.java
@@ -23,6 +23,9 @@
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
@@ -34,13 +37,28 @@
 import android.graphics.drawable.AdaptiveIconDrawable;
 import android.graphics.drawable.ColorDrawable;
 import android.os.Build;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.util.TypedValue;
+import android.util.Xml;
 import android.view.ViewOutlineProvider;
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.util.Themes;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.annotation.Nullable;
 
 /**
  * Abstract representation of the shape of a folder icon
@@ -53,15 +71,7 @@
         return sInstance;
     }
 
-    private static FolderShape[] getAllShapes() {
-        return new FolderShape[] {
-                new Circle(),
-                new RoundedSquare(8f / 50),  // Ratios based on path defined in config_icon_mask
-                new RoundedSquare(30f / 50),
-                new Square(),
-                new TearDrop(),
-                new Squircle()};
-    }
+    private SparseArray<TypedValue> mAttrs;
 
     public abstract void drawShape(Canvas canvas, float offsetX, float offsetY, float radius,
             Paint paint);
@@ -71,6 +81,11 @@
     public abstract Animator createRevealAnimator(Folder target, Rect startRect, Rect endRect,
             float endRadius, boolean isReversed);
 
+    @Nullable
+    public TypedValue getAttrValue(int attr) {
+        return mAttrs == null ? null : mAttrs.get(attr);
+    }
+
     /**
      * Abstract shape where the reveal animation is a derivative of a round rect animation
      */
@@ -163,44 +178,22 @@
         }
     }
 
-    public static class Square extends SimpleRectShape {
-
-        @Override
-        public void drawShape(Canvas canvas, float offsetX, float offsetY, float radius,  Paint p) {
-            float cx = radius + offsetX;
-            float cy = radius + offsetY;
-            canvas.drawRect(cx - radius, cy - radius, cx + radius, cy + radius, p);
-        }
-
-        @Override
-        public void addShape(Path path, float offsetX, float offsetY, float radius) {
-            float cx = radius + offsetX;
-            float cy = radius + offsetY;
-            path.addRect(cx - radius, cy - radius, cx + radius, cy + radius, Path.Direction.CW);
-        }
-
-        @Override
-        protected float getStartRadius(Rect startRect) {
-            return 0;
-        }
-    }
-
     public static class RoundedSquare extends SimpleRectShape {
 
         /**
-         * Ratio of corner radius to half size. Based on the
+         * Ratio of corner radius to half size.
          */
-        private final float mRadiusFactor;
+        private final float mRadiusRatio;
 
-        public RoundedSquare(float radiusFactor) {
-            mRadiusFactor = radiusFactor;
+        public RoundedSquare(float radiusRatio) {
+            mRadiusRatio = radiusRatio;
         }
 
         @Override
         public void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint p) {
             float cx = radius + offsetX;
             float cy = radius + offsetY;
-            float cr = radius * mRadiusFactor;
+            float cr = radius * mRadiusRatio;
             canvas.drawRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr, p);
         }
 
@@ -208,14 +201,14 @@
         public void addShape(Path path, float offsetX, float offsetY, float radius) {
             float cx = radius + offsetX;
             float cy = radius + offsetY;
-            float cr = radius * mRadiusFactor;
+            float cr = radius * mRadiusRatio;
             path.addRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr,
                     Path.Direction.CW);
         }
 
         @Override
         protected float getStartRadius(Rect startRect) {
-            return (startRect.width() / 2f) * mRadiusFactor;
+            return (startRect.width() / 2f) * mRadiusRatio;
         }
     }
 
@@ -224,13 +217,16 @@
         /**
          * Radio of short radius to large radius, based on the shape options defined in the config.
          */
-        private static final float RADIUS_RATIO = 15f / 50;
-
+        private final float mRadiusRatio;
         private final float[] mTempRadii = new float[8];
 
+        public TearDrop(float radiusRatio) {
+            mRadiusRatio = radiusRatio;
+        }
+
         @Override
         public void addShape(Path p, float offsetX, float offsetY, float r1) {
-            float r2 = r1 * RADIUS_RATIO;
+            float r2 = r1 * mRadiusRatio;
             float cx = r1 + offsetX;
             float cy = r1 + offsetY;
 
@@ -249,7 +245,7 @@
         protected AnimatorUpdateListener newUpdateListener(Rect startRect, Rect endRect,
                 float endRadius, Path outPath) {
             float r1 = startRect.width() / 2f;
-            float r2 = r1 * RADIUS_RATIO;
+            float r2 = r1 * mRadiusRatio;
 
             float[] startValues = new float[] {
                     startRect.left, startRect.top, startRect.right, startRect.bottom, r1, r2};
@@ -273,13 +269,17 @@
         /**
          * Radio of radius to circle radius, based on the shape options defined in the config.
          */
-        private static final float RADIUS_RATIO = 10f / 50;
+        private final float mRadiusRatio;
+
+        public Squircle(float radiusRatio) {
+            mRadiusRatio = radiusRatio;
+        }
 
         @Override
         public void addShape(Path p, float offsetX, float offsetY, float r) {
             float cx = r + offsetX;
             float cy = r + offsetY;
-            float control = r - r * RADIUS_RATIO;
+            float control = r - r * mRadiusRatio;
 
             p.moveTo(cx, cy - r);
             addLeftCurve(cx, cy, r, control, p);
@@ -310,7 +310,7 @@
             float startCX = startRect.exactCenterX();
             float startCY = startRect.exactCenterY();
             float startR = startRect.width() / 2f;
-            float startControl = startR - startR * RADIUS_RATIO;
+            float startControl = startR - startR * mRadiusRatio;
             float startHShift = 0;
             float startVShift = 0;
 
@@ -351,17 +351,63 @@
     }
 
     /**
-     * Initializes the shape which is closest to closest to the {@link AdaptiveIconDrawable}
+     * Initializes the shape which is closest to the {@link AdaptiveIconDrawable}
      */
-    public static void init() {
+    public static void init(Context context) {
         if (!Utilities.ATLEAST_OREO) {
             return;
         }
-        new MainThreadExecutor().execute(FolderShape::pickShapeInBackground);
+        new MainThreadExecutor().execute(() -> pickShapeInBackground(context));
+    }
+
+    private static FolderShape getShapeDefinition(String type, float radius) {
+        switch (type) {
+            case "Circle":
+                return new Circle();
+            case "RoundedSquare":
+                return new RoundedSquare(radius);
+            case "TearDrop":
+                return new TearDrop(radius);
+            case "Squircle":
+                return new Squircle(radius);
+            default:
+                throw new IllegalArgumentException("Invalid shape type: " + type);
+        }
+    }
+
+    private static List<FolderShape> getAllShapes(Context context) {
+        ArrayList<FolderShape> result = new ArrayList<>();
+        try (XmlResourceParser parser = context.getResources().getXml(R.xml.folder_shapes)) {
+
+            // Find the root tag
+            int type;
+            while ((type = parser.next()) != XmlPullParser.END_TAG
+                    && type != XmlPullParser.END_DOCUMENT
+                    && !"shapes".equals(parser.getName()));
+
+            final int depth = parser.getDepth();
+            int[] radiusAttr = new int[] {R.attr.folderIconRadius};
+            while (((type = parser.next()) != XmlPullParser.END_TAG ||
+                    parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+
+                if (type == XmlPullParser.START_TAG) {
+                    AttributeSet attrs = Xml.asAttributeSet(parser);
+                    TypedArray a = context.obtainStyledAttributes(attrs, radiusAttr);
+                    FolderShape shape = getShapeDefinition(parser.getName(), a.getFloat(0, 1));
+                    a.recycle();
+
+                    shape.mAttrs = Themes.createValueMap(context, attrs);
+                    result.add(shape);
+                }
+            }
+        } catch (IOException | XmlPullParserException e) {
+            throw new RuntimeException(e);
+        }
+        return result;
     }
 
     @TargetApi(Build.VERSION_CODES.O)
-    protected static void pickShapeInBackground() {
+    protected static void pickShapeInBackground(Context context) {
         // Pick any large size
         int size = 200;
 
@@ -379,7 +425,7 @@
         // Find the shape with minimum area of divergent region.
         int minArea = Integer.MAX_VALUE;
         FolderShape closestShape = null;
-        for (FolderShape shape : getAllShapes()) {
+        for (FolderShape shape : getAllShapes(context)) {
             shapePath.reset();
             shape.addShape(shapePath, 0, 0, size / 2f);
             shapeR.setPath(shapePath, full);
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
index 5fc5551..0d55301 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -62,7 +62,7 @@
         final IntArray addedWorkspaceScreensFinal = new IntArray();
 
         synchronized(dataModel) {
-            IntArray workspaceScreens = dataModel.workspaceScreens.clone();
+            IntArray workspaceScreens = dataModel.collectWorkspaceScreens();
 
             List<ItemInfo> filteredItems = new ArrayList<>();
             for (Pair<ItemInfo, Object> entry : mItemList) {
diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java
index f6e220f..23c6faf 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -92,7 +92,7 @@
         synchronized (mBgDataModel) {
             workspaceItems.addAll(mBgDataModel.workspaceItems);
             appWidgets.addAll(mBgDataModel.appWidgets);
-            orderedScreenIds.addAll(mBgDataModel.workspaceScreens);
+            orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
             mBgDataModel.lastBindId++;
         }
 
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 151d6f4..b338fff 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -27,6 +27,7 @@
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.Workspace;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.DumpTargetWrapper;
 import com.android.launcher3.model.nano.LauncherDumpProto;
@@ -37,6 +38,7 @@
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.google.protobuf.nano.MessageNano;
 
@@ -81,11 +83,6 @@
     public final IntSparseArrayMap<FolderInfo> folders = new IntSparseArrayMap<>();
 
     /**
-     * Ordered list of workspace screens ids.
-     */
-    public final IntArray workspaceScreens = new IntArray();
-
-    /**
      * Map of ShortcutKey to the number of times it is pinned.
      */
     public final Map<ShortcutKey, MutableInt> pinnedShortcutCounts = new HashMap<>();
@@ -118,11 +115,26 @@
         appWidgets.clear();
         folders.clear();
         itemsIdMap.clear();
-        workspaceScreens.clear();
         pinnedShortcutCounts.clear();
         deepShortcutMap.clear();
     }
 
+    /**
+     * Creates an array of valid workspace screens based on current items in the model.
+     */
+    public synchronized IntArray collectWorkspaceScreens() {
+        IntSet screenSet = new IntSet();
+        for (ItemInfo item: itemsIdMap) {
+            if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
+                screenSet.add(item.screenId);
+            }
+        }
+        if (FeatureFlags.QSB_ON_FIRST_SCREEN.get() || screenSet.isEmpty()) {
+            screenSet.add(Workspace.FIRST_SCREEN_ID);
+        }
+        return screenSet.getArray();
+    }
+
     public synchronized void dump(String prefix, FileDescriptor fd, PrintWriter writer,
             String[] args) {
         if (Arrays.asList(args).contains("--proto")) {
@@ -130,11 +142,6 @@
             return;
         }
         writer.println(prefix + "Data Model:");
-        writer.print(prefix + " ---- workspace screens: ");
-        for (int i = 0; i < workspaceScreens.size(); i++) {
-            writer.print(" " + workspaceScreens.get(i));
-        }
-        writer.println();
         writer.println(prefix + " ---- workspace items ");
         for (int i = 0; i < workspaceItems.size(); i++) {
             writer.println(prefix + '\t' + workspaceItems.get(i).toString());
@@ -167,6 +174,7 @@
         // Add top parent nodes. (L1)
         DumpTargetWrapper hotseat = new DumpTargetWrapper(ContainerType.HOTSEAT, 0);
         IntSparseArrayMap<DumpTargetWrapper> workspaces = new IntSparseArrayMap<>();
+        IntArray workspaceScreens = collectWorkspaceScreens();
         for (int i = 0; i < workspaceScreens.size(); i++) {
             workspaces.put(workspaceScreens.get(i),
                     new DumpTargetWrapper(ContainerType.WORKSPACE, i));
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 8a6aa1af..cfabc10 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -31,7 +31,6 @@
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionInfo;
-import android.graphics.Bitmap;
 import android.os.Handler;
 import android.os.Process;
 import android.os.UserHandle;
@@ -43,19 +42,17 @@
 import com.android.launcher3.AllAppsList;
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.FolderInfo;
-import com.android.launcher3.ItemInfoWithIcon;
-import com.android.launcher3.icons.ComponentWithLabel;
-import com.android.launcher3.icons.ComponentWithLabel.ComponentCachingLogic;
-import com.android.launcher3.icons.cache.IconCacheUpdateHandler;
-import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.InstallShortcutReceiver;
 import com.android.launcher3.ItemInfo;
+import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.ShortcutInfo;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.PackageInstallerCompat;
@@ -63,16 +60,18 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIconPreviewVerifier;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.ComponentWithLabel.ComponentCachingLogic;
+import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.LauncherActivtiyCachingLogic;
 import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.icons.cache.IconCacheUpdateHandler;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.provider.ImportDataTask;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutInfoCompat;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.LooperIdleLock;
 import com.android.launcher3.util.MultiHashMap;
 import com.android.launcher3.util.PackageManagerHelper;
@@ -158,9 +157,9 @@
             allItems.addAll(mBgDataModel.workspaceItems);
             allItems.addAll(mBgDataModel.appWidgets);
         }
-        int firstScreen = mBgDataModel.workspaceScreens.isEmpty()
-                ? -1 // In this case, we can still look at the items in the hotseat.
-                : mBgDataModel.workspaceScreens.get(0);
+        // Screen set is never empty
+        final int firstScreen = mBgDataModel.collectWorkspaceScreens().get(0);
+
         filterCurrentWorkspaceItems(firstScreen, allItems, firstScreenItems,
                 new ArrayList<>() /* otherScreenItems are ignored */);
         mFirstScreenBroadcast.sendBroadcasts(mApp.getContext(), firstScreenItems);
@@ -784,16 +783,6 @@
                         null,
                         new Handler(LauncherModel.getWorkerLooper()));
             }
-
-            // Initialize the screens array. Using an InstSet ensures that the screen ids
-            // are sorted.
-            IntSet screenSet = new IntSet();
-            for (ItemInfo item: mBgDataModel.itemsIdMap) {
-                if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
-                    screenSet.add(item.screenId);
-                }
-            }
-            mBgDataModel.workspaceScreens.addAll(screenSet.getArray());
         }
     }
 
diff --git a/src/com/android/launcher3/states/RotationHelper.java b/src/com/android/launcher3/states/RotationHelper.java
index 9c4a4ea..65103f6 100644
--- a/src/com/android/launcher3/states/RotationHelper.java
+++ b/src/com/android/launcher3/states/RotationHelper.java
@@ -29,6 +29,7 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.util.UiThreadHelper;
 
 /**
  * Utility class to manage launcher rotation
@@ -154,7 +155,7 @@
         }
         if (activityFlags != mLastActivityFlags) {
             mLastActivityFlags = activityFlags;
-            mActivity.setRequestedOrientation(activityFlags);
+            UiThreadHelper.setOrientationAsync(mActivity, activityFlags);
         }
     }
 
diff --git a/src/com/android/launcher3/util/Themes.java b/src/com/android/launcher3/util/Themes.java
index 5f965a3..e3ab1a5 100644
--- a/src/com/android/launcher3/util/Themes.java
+++ b/src/com/android/launcher3/util/Themes.java
@@ -21,6 +21,9 @@
 import android.graphics.Color;
 import android.graphics.ColorMatrix;
 import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.util.TypedValue;
 
 /**
  * Various utility methods associated with theming.
@@ -104,4 +107,26 @@
         target.getArray()[14] = Color.blue(dstColor) - Color.blue(srcColor);
         target.getArray()[19] = Color.alpha(dstColor) - Color.alpha(srcColor);
     }
+
+    /**
+     * Creates a map for attribute-name to value for all the values in {@param attrs} which can be
+     * held in memory for later use.
+     */
+    public static SparseArray<TypedValue> createValueMap(Context context, AttributeSet attrSet) {
+        int count = attrSet.getAttributeCount();
+        int[] attrNames = new int[count];
+        for (int i = 0; i < count; i++) {
+            attrNames[i] = attrSet.getAttributeNameResource(i);
+        }
+
+        SparseArray<TypedValue> result = new SparseArray<>(count);
+        TypedArray ta = context.obtainStyledAttributes(attrSet, attrNames);
+        for (int i = 0; i < count; i++) {
+            TypedValue tv = new TypedValue();
+            ta.getValue(i, tv);
+            result.put(attrNames[i], tv);
+        }
+
+        return result;
+    }
 }
diff --git a/src/com/android/launcher3/util/UiThreadHelper.java b/src/com/android/launcher3/util/UiThreadHelper.java
index 27140a1..cc442f9 100644
--- a/src/com/android/launcher3/util/UiThreadHelper.java
+++ b/src/com/android/launcher3/util/UiThreadHelper.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.util;
 
+import android.app.Activity;
 import android.content.Context;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -33,6 +34,8 @@
     private static Handler sHandler;
 
     private static final int MSG_HIDE_KEYBOARD = 1;
+    private static final int MSG_SET_ORIENTATION = 2;
+    private static final int MSG_RUN_COMMAND = 3;
 
     public static Looper getBackgroundLooper() {
         if (sHandlerThread == null) {
@@ -55,6 +58,15 @@
         Message.obtain(getHandler(context), MSG_HIDE_KEYBOARD, token).sendToTarget();
     }
 
+    public static void setOrientationAsync(Activity activity, int orientation) {
+        Message.obtain(getHandler(activity), MSG_SET_ORIENTATION, orientation, 0, activity)
+                .sendToTarget();
+    }
+
+    public static void runAsyncCommand(Context context, AsyncCommand command, int arg1, int arg2) {
+        Message.obtain(getHandler(context), MSG_RUN_COMMAND, arg1, arg2, command).sendToTarget();
+    }
+
     private static class UiCallbacks implements Handler.Callback {
 
         private final InputMethodManager mIMM;
@@ -69,8 +81,19 @@
                 case MSG_HIDE_KEYBOARD:
                     mIMM.hideSoftInputFromWindow((IBinder) message.obj, 0);
                     return true;
+                case MSG_SET_ORIENTATION:
+                    ((Activity) message.obj).setRequestedOrientation(message.arg1);
+                    return true;
+                case MSG_RUN_COMMAND:
+                    ((AsyncCommand) message.obj).execute(message.arg1, message.arg2);
+                    return true;
             }
             return false;
         }
     }
+
+    public interface AsyncCommand {
+
+        void execute(int arg1, int arg2);
+    }
 }