Support 4-finger gesture to quick switch

Fixes: 270610237
Test: 4-finger horizontal swipe -> quick switch
Change-Id: I981a7e93ecd07357c7a9cda075f476b397c448cc
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index f1c4f68..80f5558 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -15,11 +15,15 @@
  */
 package com.android.launcher3.uioverrides.touchcontrollers;
 
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+
 import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
 import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME;
+import static com.android.launcher3.MotionEventsUtils.isTrackpadFourFingerSwipe;
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
 import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD;
 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
@@ -106,6 +110,7 @@
             newCancelListener(this::clearState);
 
     private boolean mNoIntercept;
+    private Boolean mIsTrackpadFourFingerSwipe;
     private LauncherState mStartState;
 
     private boolean mIsHomeScreenVisible = true;
@@ -131,7 +136,9 @@
 
     @Override
     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+        int action = ev.getActionMasked();
+        if (action == ACTION_DOWN) {
+            mIsTrackpadFourFingerSwipe = null;
             mNoIntercept = !canInterceptTouch(ev);
             if (mNoIntercept) {
                 return false;
@@ -140,6 +147,13 @@
             // Only detect horizontal swipe for intercept, then we will allow swipe up as well.
             mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT,
                     false /* ignoreSlopWhenSettling */);
+        } else if (isTrackpadMultiFingerSwipe(ev) && mIsTrackpadFourFingerSwipe == null
+                && action == ACTION_MOVE) {
+            mIsTrackpadFourFingerSwipe = isTrackpadFourFingerSwipe(ev);
+            mNoIntercept = !mIsTrackpadFourFingerSwipe;
+            if (mNoIntercept) {
+                return false;
+            }
         }
 
         if (mNoIntercept) {
@@ -162,9 +176,6 @@
         if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) {
             return false;
         }
-        if (isTrackpadMultiFingerSwipe(ev)) {
-            return false;
-        }
         int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
         if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
             return false;
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 9795670..534eec1 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -2063,8 +2063,8 @@
                 mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController,
                         mRecentsAnimationTargets));
 
-        // Disable scrolling in RecentsView for trackpad gestures.
-        if (!mGestureState.isTrackpadGesture()) {
+        // Disable scrolling in RecentsView for trackpad 3-finger swipe up gesture.
+        if (!mGestureState.isThreeFingerTrackpadGesture()) {
             mRecentsViewScrollLinked = true;
         }
     }
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 2b0623a..02f9f57 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -15,6 +15,9 @@
  */
 package com.android.quickstep;
 
+import static com.android.launcher3.MotionEventsUtils.isTrackpadFourFingerSwipe;
+import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
+import static com.android.launcher3.MotionEventsUtils.isTrackpadThreeFingerSwipe;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
@@ -27,6 +30,7 @@
 import android.annotation.TargetApi;
 import android.content.Intent;
 import android.os.Build;
+import android.view.MotionEvent;
 import android.view.RemoteAnimationTarget;
 
 import com.android.launcher3.statemanager.BaseState;
@@ -139,8 +143,30 @@
     private final BaseActivityInterface mActivityInterface;
     private final MultiStateCallback mStateCallback;
     private final int mGestureId;
-    private boolean mIsTrackpadGesture;
 
+    public enum TrackpadGestureType {
+        NONE,
+        // Assigned before we know whether it's a 3-finger or 4-finger gesture.
+        MULTI_FINGER,
+        THREE_FINGER,
+        FOUR_FINGER;
+
+        public static TrackpadGestureType getTrackpadGestureType(MotionEvent event) {
+            if (!isTrackpadMultiFingerSwipe(event)) {
+                return TrackpadGestureType.NONE;
+            }
+            if (isTrackpadThreeFingerSwipe(event)) {
+                return TrackpadGestureType.THREE_FINGER;
+            }
+            if (isTrackpadFourFingerSwipe(event)) {
+                return TrackpadGestureType.FOUR_FINGER;
+            }
+
+            return TrackpadGestureType.MULTI_FINGER;
+        }
+    }
+
+    private TrackpadGestureType mTrackpadGestureType = TrackpadGestureType.NONE;
     private CachedTaskInfo mRunningTask;
     private GestureEndTarget mEndTarget;
     private RemoteAnimationTarget mLastAppearedTaskTarget;
@@ -249,17 +275,22 @@
     }
 
     /**
-     * Sets if the gesture is is from the trackpad.
+     * Sets if the gesture is is from the trackpad, if so, whether 3-finger, or 4-finger
      */
-    public void setIsTrackpadGesture(boolean isTrackpadGesture) {
-        mIsTrackpadGesture = isTrackpadGesture;
+    public void setTrackpadGestureType(TrackpadGestureType trackpadGestureType) {
+        mTrackpadGestureType = trackpadGestureType;
     }
 
-    /**
-     * @return if the gesture is from the trackpad.
-     */
     public boolean isTrackpadGesture() {
-        return mIsTrackpadGesture;
+        return mTrackpadGestureType != TrackpadGestureType.NONE;
+    }
+
+    public boolean isThreeFingerTrackpadGesture() {
+        return mTrackpadGestureType == TrackpadGestureType.THREE_FINGER;
+    }
+
+    public boolean isFourFingerTrackpadGesture() {
+        return mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER;
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index fbe2778..07db194 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -249,7 +249,7 @@
         }
 
         GestureState gestureState = mService.createGestureState(GestureState.DEFAULT_STATE,
-                false /* isTrackpadGesture */);
+                GestureState.TrackpadGestureType.NONE);
         gestureState.setHandlingAtomicEvent(true);
         AbsSwipeUpHandler interactionHandler = mService.getSwipeUpHandlerFactory()
                 .newHandler(gestureState, cmd.createTime);
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 0531b47..038c674 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -24,11 +24,11 @@
 import static android.view.MotionEvent.ACTION_UP;
 
 import static com.android.launcher3.Launcher.INTENT_ACTION_ALL_APPS_TOGGLE;
-import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
 import static com.android.launcher3.config.FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TRACKPAD_GESTURE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.GestureState.DEFAULT_STATE;
+import static com.android.quickstep.GestureState.TrackpadGestureType.getTrackpadGestureType;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_MOVE;
@@ -647,7 +647,7 @@
                 // onConsumerInactive and wipe the previous gesture state
                 GestureState prevGestureState = new GestureState(mGestureState);
                 GestureState newGestureState = createGestureState(mGestureState,
-                        isTrackpadMultiFingerSwipe(event));
+                        getTrackpadGestureType(event));
                 newGestureState.setSwipeUpStartTimeMs(SystemClock.uptimeMillis());
                 mConsumer.onConsumerAboutToBeSwitched();
                 mGestureState = newGestureState;
@@ -656,7 +656,7 @@
             } else if (mDeviceState.isUserUnlocked() && mDeviceState.isFullyGesturalNavMode()
                     && mDeviceState.canTriggerAssistantAction(event)) {
                 mGestureState = createGestureState(mGestureState,
-                        isTrackpadMultiFingerSwipe(event));
+                        getTrackpadGestureType(event));
                 // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we
                 // should not interrupt it. QuickSwitch assumes that interruption can only
                 // happen if the next gesture is also quick switch.
@@ -713,9 +713,13 @@
             event.setAction(ACTION_CANCEL);
         }
 
-        // Skip ACTION_POINTER_DOWN and ACTION_POINTER_UP events from trackpad.
-        if (!mGestureState.isTrackpadGesture() || (action != ACTION_POINTER_DOWN
-                && action != ACTION_POINTER_UP)) {
+        if (mGestureState.isTrackpadGesture() && (action == ACTION_POINTER_DOWN
+                || action == ACTION_POINTER_UP)) {
+            // Skip ACTION_POINTER_DOWN and ACTION_POINTER_UP events from trackpad.
+            if (action == ACTION_POINTER_DOWN) {
+                mGestureState.setTrackpadGestureType(getTrackpadGestureType(event));
+            }
+        } else {
             mUncheckedConsumer.onMotionEvent(event);
         }
 
@@ -749,7 +753,7 @@
     }
 
     public GestureState createGestureState(GestureState previousGestureState,
-            boolean isTrackpadGesture) {
+            GestureState.TrackpadGestureType trackpadGestureType) {
         final GestureState gestureState;
         TopTaskTracker.CachedTaskInfo taskInfo;
         if (mTaskAnimationManager.isRecentsAnimationRunning()) {
@@ -766,7 +770,7 @@
             taskInfo = TopTaskTracker.INSTANCE.get(this).getCachedTopTask(false);
             gestureState.updateRunningTask(taskInfo);
         }
-        gestureState.setIsTrackpadGesture(isTrackpadGesture);
+        gestureState.setTrackpadGestureType(trackpadGestureType);
 
         // Log initial state for the gesture.
         ActiveGestureLog.INSTANCE.addLog(new CompoundString("Current running task package name=")
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index a8963f6..2dcbbb9 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -203,8 +203,24 @@
         }
         int edgeFlags = ev.getEdgeFlags();
         ev.setEdgeFlags(edgeFlags | EDGE_NAV_BAR);
-        // Disable scrolling in RecentsView for trackpad gestures.
-        if (!mGestureState.isTrackpadGesture()) {
+
+        if (mGestureState.isTrackpadGesture()) {
+            // Disable scrolling in RecentsView for 3-finger trackpad gesture. We don't know if a
+            // trackpad motion event is 3-finger or 4-finger with the U API until ACTION_MOVE (we
+            // skip ACTION_POINTER_UP events in TouchInteractionService), so in order to make sure
+            // that RecentsView always get a closed sequence of motion events and yet disable
+            // 3-finger scroll, we do the following (1) always dispatch ACTION_DOWN and ACTION_UP
+            // trackpad multi-finger motion events. (2) only dispatch 4-finger ACTION_MOVE motion
+            // events.
+            switch (ev.getActionMasked()) {
+                case ACTION_MOVE -> {
+                    if (mGestureState.isFourFingerTrackpadGesture()) {
+                        mRecentsViewDispatcher.dispatchEvent(ev);
+                    }
+                }
+                default -> mRecentsViewDispatcher.dispatchEvent(ev);
+            }
+        } else {
             mRecentsViewDispatcher.dispatchEvent(ev);
         }
         ev.setEdgeFlags(edgeFlags);
@@ -312,9 +328,12 @@
                         // Do not allow quick switch for trackpad 3-finger gestures
                         // TODO(b/261815244): might need to impose stronger conditions for the swipe
                         //  angle
-                        boolean noQuickSwitchForTrackpadGesture = mGestureState.isTrackpadGesture()
-                                && isLikelyToStartNewTask;
-                        if (isHorizontalSwipeWhenDisabled || noQuickSwitchForTrackpadGesture) {
+                        boolean noQuickSwitchForThreeFingerGesture = isLikelyToStartNewTask
+                                && mGestureState.isThreeFingerTrackpadGesture();
+                        boolean noQuickstepForFourFingerGesture = !isLikelyToStartNewTask
+                                && mGestureState.isFourFingerTrackpadGesture();
+                        if (isHorizontalSwipeWhenDisabled || noQuickSwitchForThreeFingerGesture
+                                || noQuickstepForFourFingerGesture) {
                             forceCancelGesture(ev);
                             break;
                         }
diff --git a/src/com/android/launcher3/MotionEventsUtils.java b/src/com/android/launcher3/MotionEventsUtils.java
index 7f8794d..40de003 100644
--- a/src/com/android/launcher3/MotionEventsUtils.java
+++ b/src/com/android/launcher3/MotionEventsUtils.java
@@ -42,6 +42,14 @@
                 && event.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE;
     }
 
+    public static boolean isTrackpadThreeFingerSwipe(MotionEvent event) {
+        return isTrackpadMultiFingerSwipe(event) && event.getPointerCount() == 3;
+    }
+
+    public static boolean isTrackpadFourFingerSwipe(MotionEvent event) {
+        return isTrackpadMultiFingerSwipe(event) && event.getPointerCount() == 4;
+    }
+
     public static boolean isTrackpadMotionEvent(MotionEvent event) {
         return isTrackpadScroll(event) || isTrackpadMultiFingerSwipe(event);
     }