Serializing all event handling from SystemUI

> EventQueue is only accessed on the binder thread
> For every new touch down, we use a new event queue to prevent race conditions
> All event hendling are serialized by event queue (including quick switch/scrub)
> Using state handlers for quick scrub

Bug: 73356768
Change-Id: Idde33a348270b8167a087129f177d5d2b54f5822
diff --git a/quickstep/src/com/android/quickstep/MotionEventQueue.java b/quickstep/src/com/android/quickstep/MotionEventQueue.java
index fae9b66..6e92d83 100644
--- a/quickstep/src/com/android/quickstep/MotionEventQueue.java
+++ b/quickstep/src/com/android/quickstep/MotionEventQueue.java
@@ -16,17 +16,22 @@
 package com.android.quickstep;
 
 import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_MASK;
 import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+
+import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB;
+import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SWITCH;
 
 import android.annotation.TargetApi;
 import android.os.Build;
+import android.util.Log;
 import android.view.Choreographer;
 import android.view.MotionEvent;
 
 import com.android.systemui.shared.system.ChoreographerCompat;
 
 import java.util.ArrayList;
-import java.util.function.Consumer;
 
 /**
  * Helper class for batching input events
@@ -34,6 +39,21 @@
 @TargetApi(Build.VERSION_CODES.O)
 public class MotionEventQueue {
 
+    private static final String TAG = "MotionEventQueue";
+
+    private static final int ACTION_VIRTUAL = ACTION_MASK - 1;
+
+    private static final int ACTION_QUICK_SWITCH =
+            ACTION_VIRTUAL | (1 << ACTION_POINTER_INDEX_SHIFT);
+    private static final int ACTION_QUICK_SCRUB_START =
+            ACTION_VIRTUAL | (2 << ACTION_POINTER_INDEX_SHIFT);
+    private static final int ACTION_QUICK_SCRUB_PROGRESS =
+            ACTION_VIRTUAL | (3 << ACTION_POINTER_INDEX_SHIFT);
+    private static final int ACTION_QUICK_SCRUB_END =
+            ACTION_VIRTUAL | (4 << ACTION_POINTER_INDEX_SHIFT);
+    private static final int ACTION_RESET =
+            ACTION_VIRTUAL | (5 << ACTION_POINTER_INDEX_SHIFT);
+
     private final EventArray mEmptyArray = new EventArray();
     private final Object mExecutionLock = new Object();
 
@@ -46,45 +66,48 @@
 
     private final Choreographer mMainChoreographer;
 
-    private Consumer<MotionEvent> mConsumer;
+    private final TouchConsumer mConsumer;
 
     private Choreographer mInterimChoreographer;
     private Choreographer mCurrentChoreographer;
 
     private Runnable mCurrentRunnable;
 
-    public MotionEventQueue(Choreographer choreographer, Consumer<MotionEvent> consumer) {
+    public MotionEventQueue(Choreographer choreographer, TouchConsumer consumer) {
         mMainChoreographer = choreographer;
         mConsumer = consumer;
 
         mCurrentChoreographer = mMainChoreographer;
         mCurrentRunnable = mMainFrameCallback;
-    }
-
-    public void setConsumer(Consumer<MotionEvent> consumer) {
-        synchronized (mExecutionLock) {
-            mConsumer = consumer;
-        }
+        setInterimChoreographerLocked(consumer.getIntrimChoreographer(this));
     }
 
     public void setInterimChoreographer(Choreographer choreographer) {
         synchronized (mExecutionLock) {
             synchronized (mArrays) {
-                mInterimChoreographer = choreographer;
-                if (choreographer == null) {
-                    mCurrentChoreographer = mMainChoreographer;
-                    mCurrentRunnable = mMainFrameCallback;
-                } else {
-                    mCurrentChoreographer = mInterimChoreographer;
-                    mCurrentRunnable = mInterimFrameCallback;
-                }
-
+                setInterimChoreographerLocked(choreographer);
                 ChoreographerCompat.postInputFrame(mCurrentChoreographer, mCurrentRunnable);
             }
         }
     }
 
+    private void  setInterimChoreographerLocked(Choreographer choreographer) {
+        mInterimChoreographer = choreographer;
+        if (choreographer == null) {
+            mCurrentChoreographer = mMainChoreographer;
+            mCurrentRunnable = mMainFrameCallback;
+        } else {
+            mCurrentChoreographer = mInterimChoreographer;
+            mCurrentRunnable = mInterimFrameCallback;
+        }
+    }
+
     public void queue(MotionEvent event) {
+        mConsumer.preProcessMotionEvent(event);
+        queueNoPreProcess(event);
+    }
+
+    private void queueNoPreProcess(MotionEvent event) {
         synchronized (mArrays) {
             EventArray array = mArrays[mCurrentIndex];
             if (array.isEmpty()) {
@@ -116,7 +139,29 @@
             int size = array.size();
             for (int i = 0; i < size; i++) {
                 MotionEvent event = array.get(i);
-                mConsumer.accept(event);
+                if (event.getActionMasked() == ACTION_VIRTUAL) {
+                    switch (event.getAction()) {
+                        case ACTION_QUICK_SWITCH:
+                            mConsumer.updateTouchTracking(INTERACTION_QUICK_SWITCH);
+                            break;
+                        case ACTION_QUICK_SCRUB_START:
+                            mConsumer.updateTouchTracking(INTERACTION_QUICK_SCRUB);
+                            break;
+                        case ACTION_QUICK_SCRUB_PROGRESS:
+                            mConsumer.onQuickScrubProgress(event.getX());
+                            break;
+                        case ACTION_QUICK_SCRUB_END:
+                            mConsumer.onQuickScrubEnd();
+                            break;
+                        case ACTION_RESET:
+                            mConsumer.reset();
+                            break;
+                        default:
+                            Log.e(TAG, "Invalid virtual event: " + event.getAction());
+                    }
+                } else {
+                    mConsumer.accept(event);
+                }
                 event.recycle();
             }
             array.clear();
@@ -135,6 +180,30 @@
         }
     }
 
+    private void queueVirtualAction(int action, float progress) {
+        queueNoPreProcess(MotionEvent.obtain(0, 0, action, progress, 0, 0));
+    }
+
+    public void onQuickSwitch() {
+        queueVirtualAction(ACTION_QUICK_SWITCH, 0);
+    }
+
+    public void onQuickScrubStart() {
+        queueVirtualAction(ACTION_QUICK_SCRUB_START, 0);
+    }
+
+    public void onQuickScrubProgress(float progress) {
+        queueVirtualAction(ACTION_QUICK_SCRUB_PROGRESS, progress);
+    }
+
+    public void onQuickScrubEnd() {
+        queueVirtualAction(ACTION_QUICK_SCRUB_END, 0);
+    }
+
+    public void reset() {
+        queueVirtualAction(ACTION_RESET, 0);
+    }
+
     private static class EventArray extends ArrayList<MotionEvent> {
 
         public int lastEventAction = ACTION_CANCEL;
diff --git a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
index 0be13ea..ae70474 100644
--- a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
+++ b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
@@ -371,10 +371,6 @@
             if (mQuickScrubController != null) {
                 mQuickScrubController.onQuickSwitch();
             }
-        } else if (mInteractionType == INTERACTION_QUICK_SCRUB) {
-            if (mQuickScrubController != null) {
-                mQuickScrubController.snapToPageForCurrentQuickScrubSection();
-            }
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
index ef7f894..1e48a53 100644
--- a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
+++ b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
@@ -40,6 +40,7 @@
 import android.os.Looper;
 import android.os.SystemClock;
 import android.util.Log;
+import android.view.Choreographer;
 import android.view.Display;
 import android.view.MotionEvent;
 import android.view.Surface;
@@ -110,6 +111,7 @@
     private final Intent mHomeIntent;
     private final ISystemUiProxy mISystemUiProxy;
     private final MainThreadExecutor mMainThreadExecutor;
+    private final Choreographer mBackgroundThreadChoreographer;
 
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
@@ -123,12 +125,13 @@
     private @HitTarget int mDownHitTarget = HIT_TARGET_NONE;
 
     private VelocityTracker mVelocityTracker;
+    private MotionEventQueue mEventQueue;
 
     private final MetricsLogger mMetricsLogger = new MetricsLogger();
 
     public OtherActivityTouchConsumer(Context base, RunningTaskInfo runningTaskInfo,
             RecentsModel recentsModel, Intent homeIntent, ISystemUiProxy systemUiProxy,
-            MainThreadExecutor mainThreadExecutor) {
+            MainThreadExecutor mainThreadExecutor, Choreographer backgroundThreadChoreographer) {
         super(base);
         mRunningTask = runningTaskInfo;
         mRecentsModel = recentsModel;
@@ -136,6 +139,7 @@
         mVelocityTracker = VelocityTracker.obtain();
         mISystemUiProxy = systemUiProxy;
         mMainThreadExecutor = mainThreadExecutor;
+        mBackgroundThreadChoreographer = backgroundThreadChoreographer;
     }
 
     @Override
@@ -394,8 +398,6 @@
         }
         mVelocityTracker.recycle();
         mVelocityTracker = null;
-
-        onTouchTrackingComplete();
     }
 
     @Override
@@ -412,16 +414,23 @@
     public void updateTouchTracking(int interactionType) {
         notifyGestureStarted();
 
-        mMainThreadExecutor.execute(() -> {
+        if (isUsingScreenShot()) {
+            mMainThreadExecutor.execute(() -> {
+                if (mInteractionHandler != null) {
+                    mInteractionHandler.updateInteractionType(interactionType);
+                }
+            });
+        } else {
             if (mInteractionHandler != null) {
                 mInteractionHandler.updateInteractionType(interactionType);
             }
-        });
+        }
     }
 
     @Override
-    public boolean shouldUseBackgroundConsumer() {
-        return !isUsingScreenShot();
+    public Choreographer getIntrimChoreographer(MotionEventQueue queue) {
+        mEventQueue = queue;
+        return isUsingScreenShot() ? null : mBackgroundThreadChoreographer;
     }
 
     @Override
@@ -444,9 +453,9 @@
         }
     }
 
-    public void onTouchTrackingComplete() { }
-
-    public void switchToMainChoreographer() { }
+    public void switchToMainChoreographer() {
+        mEventQueue.setInterimChoreographer(null);
+    }
 
     @Override
     public void preProcessMotionEvent(MotionEvent ev) {
diff --git a/quickstep/src/com/android/quickstep/QuickScrubController.java b/quickstep/src/com/android/quickstep/QuickScrubController.java
index 8d5422f..bc7647a 100644
--- a/quickstep/src/com/android/quickstep/QuickScrubController.java
+++ b/quickstep/src/com/android/quickstep/QuickScrubController.java
@@ -38,9 +38,10 @@
     private static final int QUICKSCRUB_SNAP_DURATION_PER_PAGE = 325;
     private static final int QUICKSCRUB_END_SNAP_DURATION_PER_PAGE = 60;
 
-    private Alarm mAutoAdvanceAlarm;
-    private RecentsView mRecentsView;
+    private final Alarm mAutoAdvanceAlarm;
+    private final RecentsView mRecentsView;
 
+    private boolean mInQuickScrub;
     private int mQuickScrubSection;
     private int mStartPage;
 
@@ -51,32 +52,30 @@
     }
 
     public void onQuickScrubStart(boolean startingFromHome) {
+        mInQuickScrub = true;
         mStartPage = startingFromHome ? 0 : mRecentsView.getFirstTaskIndex();
         mQuickScrubSection = 0;
     }
 
     public void onQuickScrubEnd() {
+        mInQuickScrub = false;
         mAutoAdvanceAlarm.cancelAlarm();
-        if (mRecentsView == null) {
-        } else {
-            int page = mRecentsView.getNextPage();
-            Runnable launchTaskRunnable = () -> {
-                if (page < mRecentsView.getFirstTaskIndex()) {
-                    // Call post() since we can't performClick() on a background thread.
-                    mRecentsView.post(() -> mRecentsView.getPageAt(page).performClick());
-                } else {
-                    ((TaskView) mRecentsView.getPageAt(page)).launchTask(true);
-                }
-            };
-            int snapDuration = Math.abs(page - mRecentsView.getPageNearestToCenterOfScreen())
-                    * QUICKSCRUB_END_SNAP_DURATION_PER_PAGE;
-            if (mRecentsView.snapToPage(page, snapDuration)) {
-                // Settle on the page then launch it
-                mRecentsView.setNextPageSwitchRunnable(launchTaskRunnable);
+        int page = mRecentsView.getNextPage();
+        Runnable launchTaskRunnable = () -> {
+            if (page < mRecentsView.getFirstTaskIndex()) {
+                mRecentsView.getPageAt(page).performClick();
             } else {
-                // No page move needed, just launch it
-                launchTaskRunnable.run();
+                ((TaskView) mRecentsView.getPageAt(page)).launchTask(true);
             }
+        };
+        int snapDuration = Math.abs(page - mRecentsView.getPageNearestToCenterOfScreen())
+                * QUICKSCRUB_END_SNAP_DURATION_PER_PAGE;
+        if (mRecentsView.snapToPage(page, snapDuration)) {
+            // Settle on the page then launch it
+            mRecentsView.setNextPageSwitchRunnable(launchTaskRunnable);
+        } else {
+            // No page move needed, just launch it
+            launchTaskRunnable.run();
         }
     }
 
@@ -112,7 +111,9 @@
     }
 
     public void snapToPageForCurrentQuickScrubSection() {
-        goToPageWithHaptic(mRecentsView.getFirstTaskIndex() + mQuickScrubSection);
+        if (mInQuickScrub) {
+            goToPageWithHaptic(mRecentsView.getFirstTaskIndex() + mQuickScrubSection);
+        }
     }
 
     private void goToPageWithHaptic(int pageToGoTo) {
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java b/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java
index 2fae01a..28229f6 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java
@@ -15,9 +15,6 @@
  */
 package com.android.quickstep;
 
-import android.os.Handler;
-import android.os.Looper;
-
 import com.android.systemui.shared.system.BackgroundExecutor;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -43,7 +40,8 @@
     }
 
     /**
-     * @param onFinishComplete A callback that runs on the UI thread.
+     * @param onFinishComplete A callback that runs after the animation controller has finished
+     *                         on the background thread.
      */
     public void finish(boolean toHome, Runnable onFinishComplete) {
         BackgroundExecutor.get().submit(() -> {
@@ -52,7 +50,7 @@
                     controller.setInputConsumerEnabled(false);
                     controller.finish(toHome);
                     if (onFinishComplete != null) {
-                        new Handler(Looper.getMainLooper()).post(onFinishComplete);
+                        onFinishComplete.run();
                     }
                 }
             }
diff --git a/quickstep/src/com/android/quickstep/RecentsView.java b/quickstep/src/com/android/quickstep/RecentsView.java
index 5718869..a9d5e63 100644
--- a/quickstep/src/com/android/quickstep/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/RecentsView.java
@@ -266,6 +266,8 @@
             return;
         }
 
+        int oldChildCount = getChildCount();
+
         // Ensure there are as many views as there are tasks in the stack (adding and trimming as
         // necessary)
         final LayoutInflater inflater = LayoutInflater.from(getContext());
@@ -296,6 +298,10 @@
             taskView.setAlpha(1f);
             loader.loadTaskData(task);
         }
+
+        if (oldChildCount != getChildCount()) {
+            mQuickScrubController.snapToPageForCurrentQuickScrubSection();
+        }
     }
 
     private void updateTaskStackListenerState() {
diff --git a/quickstep/src/com/android/quickstep/TouchConsumer.java b/quickstep/src/com/android/quickstep/TouchConsumer.java
index 5cfa8df..aaea379 100644
--- a/quickstep/src/com/android/quickstep/TouchConsumer.java
+++ b/quickstep/src/com/android/quickstep/TouchConsumer.java
@@ -18,6 +18,7 @@
 import android.annotation.TargetApi;
 import android.os.Build;
 import android.support.annotation.IntDef;
+import android.view.Choreographer;
 import android.view.MotionEvent;
 
 import com.android.systemui.shared.system.NavigationBarCompat.HitTarget;
@@ -48,10 +49,6 @@
 
     default void reset() { }
 
-    default boolean shouldUseBackgroundConsumer() {
-        return false;
-    }
-
     default void setDownHitTarget(@HitTarget int downHitTarget) { }
 
     default void updateTouchTracking(@InteractionType int interactionType) { }
@@ -65,4 +62,8 @@
      * posted on a handler thread.
      */
     default void preProcessMotionEvent(MotionEvent ev) { }
+
+    default Choreographer getIntrimChoreographer(MotionEventQueue queue) {
+        return null;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 52cd60e..46ef528 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -23,8 +23,6 @@
 import static android.view.MotionEvent.ACTION_UP;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.quickstep.QuickScrubController.QUICK_SWITCH_START_DURATION;
-import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB;
-import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SWITCH;
 
 import android.annotation.TargetApi;
 import android.app.ActivityManager.RunningTaskInfo;
@@ -77,7 +75,7 @@
 
         @Override
         public void onMotionEvent(MotionEvent ev) {
-            onBinderMotionEvent(ev);
+            mEventQueue.queue(ev);
         }
 
         @Override
@@ -90,29 +88,28 @@
 
         @Override
         public void onQuickSwitch() {
-            mCurrentConsumer.updateTouchTracking(INTERACTION_QUICK_SWITCH);
+            mEventQueue.onQuickSwitch();
         }
 
         @Override
         public void onQuickScrubStart() {
-            mCurrentConsumer.updateTouchTracking(INTERACTION_QUICK_SCRUB);
+            mEventQueue.onQuickScrubStart();
             sQuickScrubEnabled = true;
         }
 
         @Override
-        public void onQuickScrubEnd() {
-            mCurrentConsumer.onQuickScrubEnd();
-            sQuickScrubEnabled = false;
+        public void onQuickScrubProgress(float progress) {
+            mEventQueue.onQuickScrubProgress(progress);
         }
 
         @Override
-        public void onQuickScrubProgress(float progress) {
-            mCurrentConsumer.onQuickScrubProgress(progress);
+        public void onQuickScrubEnd() {
+            mEventQueue.onQuickScrubEnd();
+            sQuickScrubEnabled = false;
         }
     };
 
     private final TouchConsumer mNoOpTouchConsumer = (ev) -> {};
-    private TouchConsumer mCurrentConsumer = mNoOpTouchConsumer;
 
     private static boolean sConnected = false;
     private static boolean sQuickScrubEnabled = false;
@@ -133,7 +130,10 @@
     private MotionEventQueue mEventQueue;
     private MainThreadExecutor mMainThreadExecutor;
     private ISystemUiProxy mISystemUiProxy;
+
+    private Choreographer mMainThreadChoreographer;
     private Choreographer mBackgroundThreadChoreographer;
+    private MotionEventQueue mNoOpEventQueue;
 
     @Override
     public void onCreate() {
@@ -151,7 +151,10 @@
         // Clear the packageName as system can fail to dedupe it b/64108432
         mHomeIntent.setComponent(mLauncher).setPackage(null);
 
-        mEventQueue = new MotionEventQueue(Choreographer.getInstance(), mNoOpTouchConsumer);
+        mMainThreadChoreographer = Choreographer.getInstance();
+        mNoOpEventQueue = new MotionEventQueue(mMainThreadChoreographer, mNoOpTouchConsumer);
+        mEventQueue = mNoOpEventQueue;
+
         sConnected = true;
 
         // Temporarily disable model preload
@@ -175,60 +178,32 @@
     private void onBinderPreMotionEvent(@HitTarget int downHitTarget) {
         mRunningTask = mAM.getRunningTask();
 
-        mCurrentConsumer.reset();
+        mEventQueue.reset();
+
         if (mRunningTask == null) {
-            mCurrentConsumer = mNoOpTouchConsumer;
+            mEventQueue = mNoOpEventQueue;
         } else if (mRunningTask.topActivity.equals(mLauncher)) {
-            mCurrentConsumer = getLauncherConsumer();
+            mEventQueue = getLauncherEventQueue();
         } else {
-            mCurrentConsumer = getOtherActivityConsumer();
+            mEventQueue = new MotionEventQueue(mMainThreadChoreographer,
+                    new OtherActivityTouchConsumer(this, mRunningTask, mRecentsModel,
+                    mHomeIntent, mISystemUiProxy, mMainThreadExecutor,
+                    mBackgroundThreadChoreographer));
         }
-
-        mCurrentConsumer.setDownHitTarget(downHitTarget);
-        mEventQueue.setConsumer(mCurrentConsumer);
-        mEventQueue.setInterimChoreographer(mCurrentConsumer.shouldUseBackgroundConsumer()
-                ? mBackgroundThreadChoreographer : null);
     }
 
-    private void onBinderMotionEvent(MotionEvent ev) {
-        mCurrentConsumer.preProcessMotionEvent(ev);
-        mEventQueue.queue(ev);
-    }
-
-    private TouchConsumer getOtherActivityConsumer() {
-        TouchConsumer consumer = new OtherActivityTouchConsumer(this, mRunningTask, mRecentsModel,
-                mHomeIntent, mISystemUiProxy, mMainThreadExecutor) {
-
-            @Override
-            public void switchToMainChoreographer() {
-                if (mCurrentConsumer == this) {
-                    mEventQueue.setInterimChoreographer(null);
-                }
-            }
-
-            @Override
-            public void onTouchTrackingComplete() {
-                if (mCurrentConsumer == this) {
-                    mCurrentConsumer = mNoOpTouchConsumer;
-                    mEventQueue.setConsumer(mCurrentConsumer);
-                }
-            }
-        };
-        return consumer;
-    }
-
-    private TouchConsumer getLauncherConsumer() {
-
+    private MotionEventQueue getLauncherEventQueue() {
         Launcher launcher = (Launcher) LauncherAppState.getInstance(this).getModel().getCallback();
         if (launcher == null) {
-            return mNoOpTouchConsumer;
+            return mNoOpEventQueue;
         }
 
         View target = launcher.getDragLayer();
-        return new LauncherTouchConsumer(launcher, target);
+        return new MotionEventQueue(mMainThreadChoreographer,
+                new LauncherTouchConsumer(launcher, target));
     }
 
-    private class LauncherTouchConsumer implements TouchConsumer {
+    private static class LauncherTouchConsumer implements TouchConsumer {
 
         private final Launcher mLauncher;
         private final View mTarget;
@@ -238,6 +213,7 @@
         private final QuickScrubController mQuickScrubController;
 
         private boolean mTrackingStarted = false;
+        private boolean mInvalidated = false;
 
         LauncherTouchConsumer(Launcher launcher, View target) {
             mLauncher = launcher;
@@ -250,6 +226,9 @@
 
         @Override
         public void accept(MotionEvent ev) {
+            if (mInvalidated) {
+                return;
+            }
             if (!mTarget.hasWindowFocus()) {
                 return;
             }
@@ -262,7 +241,7 @@
                     case ACTION_POINTER_UP:
                     case ACTION_POINTER_DOWN:
                         if (!mTrackingStarted) {
-                            mEventQueue.setConsumer(mNoOpTouchConsumer);
+                            mInvalidated = true;
                         }
                         break;
                     case ACTION_MOVE: {
@@ -286,7 +265,7 @@
             }
 
             if (action == ACTION_UP || action == ACTION_CANCEL) {
-                mEventQueue.setConsumer(mNoOpTouchConsumer);
+                mInvalidated = true;
             }
         }
 
@@ -301,35 +280,42 @@
 
         @Override
         public void updateTouchTracking(int interactionType) {
-            mMainThreadExecutor.execute(() -> {
-                if (TouchConsumer.isInteractionQuick(interactionType)) {
-                    Runnable action = () -> {
-                        Runnable onComplete = null;
-                        if (interactionType == INTERACTION_QUICK_SCRUB) {
-                            mQuickScrubController.onQuickScrubStart(true);
-                        } else if (interactionType == INTERACTION_QUICK_SWITCH) {
-                            onComplete = mQuickScrubController::onQuickSwitch;
-                        }
-                        mLauncher.getStateManager().goToState(OVERVIEW, true, 0,
-                                QUICK_SWITCH_START_DURATION, onComplete);
-                    };
-
-                    if (mLauncher.getWorkspace().runOnOverlayHidden(action)) {
-                        // Hide the minus one overlay so launcher can get window focus.
-                        mLauncher.onQuickstepGestureStarted(true);
+            if (mInvalidated) {
+                return;
+            }
+            if (TouchConsumer.isInteractionQuick(interactionType)) {
+                Runnable action = () -> {
+                    Runnable onComplete = null;
+                    if (interactionType == INTERACTION_QUICK_SCRUB) {
+                        mQuickScrubController.onQuickScrubStart(true);
+                    } else if (interactionType == INTERACTION_QUICK_SWITCH) {
+                        onComplete = mQuickScrubController::onQuickSwitch;
                     }
+                    mLauncher.getStateManager().goToState(OVERVIEW, true, 0,
+                            QUICK_SWITCH_START_DURATION, onComplete);
+                };
+
+                if (mLauncher.getWorkspace().runOnOverlayHidden(action)) {
+                    // Hide the minus one overlay so launcher can get window focus.
+                    mLauncher.onQuickstepGestureStarted(true);
                 }
-            });
+            }
         }
 
         @Override
         public void onQuickScrubEnd() {
-            mMainThreadExecutor.execute(mQuickScrubController::onQuickScrubEnd);
+            if (mInvalidated) {
+                return;
+            }
+            mQuickScrubController.onQuickScrubEnd();
         }
 
         @Override
         public void onQuickScrubProgress(float progress) {
-            mMainThreadExecutor.execute(() -> mQuickScrubController.onQuickScrubProgress(progress));
+            if (mInvalidated) {
+                return;
+            }
+            mQuickScrubController.onQuickScrubProgress(progress);
         }
 
     }
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index a4c8d96..6b1c5e1 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -59,7 +59,6 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.TouchConsumer.InteractionType;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -91,6 +90,13 @@
     private static final int STATE_HANDLER_INVALIDATED = 1 << 6;
     private static final int STATE_GESTURE_STARTED = 1 << 7;
 
+    // States for quick switch/scrub
+    private static final int STATE_SWITCH_TO_SCREENSHOT_COMPLETE = 1 << 8;
+    private static final int STATE_QUICK_SWITCH = 1 << 9;
+    private static final int STATE_QUICK_SCRUB_START = 1 << 10;
+    private static final int STATE_QUICK_SCRUB_END = 1 << 11;
+
+
     private static final int LAUNCHER_UI_STATES =
             STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_ACTIVITY_MULTIPLIER_COMPLETE;
 
@@ -103,7 +109,11 @@
             "STATE_SCALED_CONTROLLER_RECENTS",
             "STATE_SCALED_CONTROLLER_APP",
             "STATE_HANDLER_INVALIDATED",
-            "STATE_GESTURE_STARTED"
+            "STATE_GESTURE_STARTED",
+            "STATE_SWITCH_TO_SCREENSHOT_COMPLETE",
+            "STATE_QUICK_SWITCH",
+            "STATE_QUICK_SCRUB_START",
+            "STATE_QUICK_SCRUB_END"
     };
 
     private static final long MAX_SWIPE_DURATION = 200;
@@ -162,7 +172,6 @@
     private boolean mGestureStarted;
 
     private @InteractionType int mInteractionType = INTERACTION_NORMAL;
-    private boolean mDeferredQuickScrubEnd;
 
     private InputConsumerController mInputConsumer =
             InputConsumerController.getRecentsAnimationInputConsumer();
@@ -185,6 +194,7 @@
                 super.setState(stateFlag);
             }
         };
+
         mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED,
                 this::initializeLauncherAnimationController);
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN,
@@ -195,8 +205,7 @@
         mStateCallback.addCallback(STATE_SCALED_CONTROLLER_APP | STATE_APP_CONTROLLER_RECEIVED,
                 this::resumeLastTask);
         mStateCallback.addCallback(STATE_SCALED_CONTROLLER_RECENTS
-                        | STATE_ACTIVITY_MULTIPLIER_COMPLETE
-                        | STATE_APP_CONTROLLER_RECEIVED,
+                | STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_APP_CONTROLLER_RECEIVED,
                 this::switchToScreenshot);
 
         mStateCallback.addCallback(STATE_SCALED_CONTROLLER_RECENTS
@@ -211,6 +220,16 @@
         mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
                 this::invalidateHandlerWithLauncher);
+
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_QUICK_SWITCH,
+                this::onQuickInteractionStart);
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_QUICK_SCRUB_START,
+                this::onQuickInteractionStart);
+
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SWITCH_TO_SCREENSHOT_COMPLETE
+                | STATE_QUICK_SWITCH, this::switchToFinalAppAfterQuickSwitch);
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SWITCH_TO_SCREENSHOT_COMPLETE
+                | STATE_QUICK_SCRUB_END, this::switchToFinalAppAfterQuickScrub);
     }
 
     private void setStateOnUiThread(int stateFlag) {
@@ -294,6 +313,7 @@
         AbstractFloatingView.closeAllOpenViews(launcher, alreadyOnHome);
 
         mRecentsView = mLauncher.getOverviewPanel();
+        mQuickScrubController = mRecentsView.getQuickScrubController();
         mLauncherLayoutListener = new LauncherLayoutListener(mLauncher);
 
         final int state;
@@ -370,7 +390,6 @@
     }
 
     public void updateInteractionType(@InteractionType int interactionType) {
-        Preconditions.assertUIThread();
         if (mInteractionType != INTERACTION_NORMAL) {
             throw new IllegalArgumentException(
                     "Can't change interaction type from " + mInteractionType);
@@ -381,16 +400,15 @@
         }
         mInteractionType = interactionType;
 
-        if (mLauncher != null) {
-            updateUiForQuickScrub();
-        }
+        setStateOnUiThread(interactionType == INTERACTION_QUICK_SWITCH
+                ? STATE_QUICK_SWITCH : STATE_QUICK_SCRUB_START);
+
+        // Start the window animation without waiting for launcher.
+        animateToProgress(1f, QUICK_SWITCH_START_DURATION);
     }
 
-    private void updateUiForQuickScrub() {
-        mDeferredQuickScrubEnd = false;
-        mQuickScrubController = mRecentsView.getQuickScrubController();
+    private void onQuickInteractionStart() {
         mQuickScrubController.onQuickScrubStart(false);
-        animateToProgress(1f, QUICK_SWITCH_START_DURATION);
     }
 
     @WorkerThread
@@ -647,21 +665,8 @@
                 transaction.apply();
             }
         }
-        mRecentsAnimationWrapper.finish(true /* toHome */, () -> {
-            if (mInteractionType == INTERACTION_QUICK_SWITCH) {
-                if (mQuickScrubController != null) {
-                    mQuickScrubController.onQuickSwitch();
-                }
-            } else if (mInteractionType == INTERACTION_QUICK_SCRUB) {
-                if (mQuickScrubController != null) {
-                    if (mDeferredQuickScrubEnd) {
-                        onQuickScrubEnd();
-                    } else {
-                        mQuickScrubController.snapToPageForCurrentQuickScrubSection();
-                    }
-                }
-            }
-        });
+        mRecentsAnimationWrapper.finish(true /* toHome */,
+                () -> setStateOnUiThread(STATE_SWITCH_TO_SCREENSHOT_COMPLETE));
     }
 
     private void setupLauncherUiAfterSwipeUpAnimation() {
@@ -676,19 +681,15 @@
     }
 
     public void onQuickScrubEnd() {
-        if ((mStateCallback.getState() & STATE_SCALED_CONTROLLER_RECENTS) == 0) {
-            // If we are still animating into recents, then defer until that has run to end
-            // quick scrub since we need to finish the window animation before launching the next
-            // task
-            mDeferredQuickScrubEnd = true;
-            return;
-        }
+        setStateOnUiThread(STATE_QUICK_SCRUB_END);
+    }
 
-        if (mQuickScrubController != null) {
-            mQuickScrubController.onQuickScrubEnd();
-        } else {
-            // TODO:
-        }
+    private void switchToFinalAppAfterQuickSwitch() {
+        mQuickScrubController.onQuickSwitch();
+    }
+
+    private void switchToFinalAppAfterQuickScrub() {
+        mQuickScrubController.onQuickScrubEnd();
 
         // Normally this is handled in reset(), but since we are still scrubbing after the
         // transition into recents, we need to defer the handler invalidation for quick scrub until
@@ -697,14 +698,16 @@
     }
 
     public void onQuickScrubProgress(float progress) {
-        if (mQuickScrubController != null) {
-            mQuickScrubController.onQuickScrubProgress(progress);
-        } else {
-            // TODO:
+        if (Looper.myLooper() != Looper.getMainLooper() || mQuickScrubController == null) {
+            // TODO: We can still get progress events while launcher is not ready on the worker
+            // thread. Keep track of last received progress and apply that progress when launcher
+            // is ready
+            return;
         }
+        mQuickScrubController.onQuickScrubProgress(progress);
     }
 
-    private synchronized void debugNewState(int stateFlag) {
+    private void debugNewState(int stateFlag) {
         if (!DEBUG_STATES) {
             return;
         }