Merge "Moved assistant gesture to corners" into ub-launcher3-master
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java
index 109a4c5..5494052 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java
@@ -22,59 +22,70 @@
 import static android.view.MotionEvent.ACTION_POINTER_UP;
 import static android.view.MotionEvent.ACTION_UP;
 
+import android.animation.ValueAnimator;
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.PointF;
-import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.util.Log;
-import android.view.Display;
 import android.view.MotionEvent;
-import android.view.Surface;
-import android.view.ViewConfiguration;
-import android.view.WindowManager;
+import com.android.launcher3.anim.Interpolators;
+import com.android.quickstep.util.MotionPauseDetector;
 import com.android.systemui.shared.recents.ISystemUiProxy;
-import com.android.systemui.shared.system.NavigationBarCompat;
-import com.android.systemui.shared.system.WindowManagerWrapper;
 import com.android.launcher3.R;
+import com.android.systemui.shared.system.NavigationBarCompat;
 
 /**
  * Touch consumer for handling events to launch assistant from launcher
  */
 public class AssistantTouchConsumer implements InputConsumer {
     private static final String TAG = "AssistantTouchConsumer";
+    private static final long RETRACT_ANIMATION_DURATION_MS = 300;
+
+    /* The assistant touch consume competes with quick switch InputConsumer gesture. The delegate
+     * can be chosen to run if the angle passing the slop is lower than the threshold angle. When
+     * this occurs, the state changes to {@link #STATE_DELEGATE_ACTIVE} where the next incoming
+     * motion events are handled by the delegate instead of the assistant touch consumer. If the
+     * angle is higher than the threshold, the state will change to {@link #STATE_ASSISTANT_ACTIVE}.
+     */
+    private static final int STATE_INACTIVE = 0;
+    private static final int STATE_ASSISTANT_ACTIVE = 1;
+    private static final int STATE_DELEGATE_ACTIVE = 2;
 
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
+    private final PointF mStartDragPos = new PointF();
+
     private int mActivePointerId = -1;
-
-    private final int mDisplayRotation;
-    private final Rect mStableInsets = new Rect();
-
-    private final float mDragSlop;
-    private final float mTouchSlop;
-    private final float mThreshold;
-
-    private float mStartDisplacement;
-    private boolean mPassedDragSlop;
-    private boolean mPassedTouchSlop;
-    private long mPassedTouchSlopTime;
+    private boolean mPassedSlop;
     private boolean mLaunchedAssistant;
+    private float mDistance;
+    private float mTimeFraction;
+    private long mDragTime;
     private float mLastProgress;
+    private int mState;
 
+    private final float mDistThreshold;
+    private final long mTimeThreshold;
+    private final int mAngleThreshold;
+    private final float mSlop;
+    private final MotionPauseDetector mMotionPauseDetector;
     private final ISystemUiProxy mSysUiProxy;
+    private final InputConsumer mConsumerDelegate;
 
-    public AssistantTouchConsumer(Context context, ISystemUiProxy systemUiProxy) {
+    public AssistantTouchConsumer(Context context, ISystemUiProxy systemUiProxy,
+            InputConsumer delegate) {
+        final Resources res = context.getResources();
         mSysUiProxy = systemUiProxy;
-
-        mDragSlop = NavigationBarCompat.getQuickStepDragSlopPx();
-        mTouchSlop = NavigationBarCompat.getQuickStepTouchSlopPx();
-        mThreshold = context.getResources().getDimension(R.dimen.gestures_assistant_threshold);
-
-        Display display = context.getSystemService(WindowManager.class).getDefaultDisplay();
-        mDisplayRotation = display.getRotation();
-        WindowManagerWrapper.getInstance().getStableInsets(mStableInsets);
+        mConsumerDelegate = delegate;
+        mMotionPauseDetector = new MotionPauseDetector(context);
+        mDistThreshold = res.getDimension(R.dimen.gestures_assistant_drag_threshold);
+        mTimeThreshold = res.getInteger(R.integer.assistant_gesture_min_time_threshold);
+        mAngleThreshold = res.getInteger(R.integer.assistant_gesture_corner_deg_threshold);
+        mSlop = NavigationBarCompat.getQuickScrubTouchSlopPx();
+        mState = STATE_INACTIVE;
     }
 
     @Override
@@ -83,14 +94,28 @@
     }
 
     @Override
+    public boolean isActive() {
+        return mState != STATE_INACTIVE;
+    }
+
+    @Override
     public void onMotionEvent(MotionEvent ev) {
         // TODO add logging
+
         switch (ev.getActionMasked()) {
             case ACTION_DOWN: {
                 mActivePointerId = ev.getPointerId(0);
                 mDownPos.set(ev.getX(), ev.getY());
                 mLastPos.set(mDownPos);
-                mLastProgress = -1;
+                mTimeFraction = 0;
+
+                // Detect when the gesture decelerates to start the assistant
+                mMotionPauseDetector.setOnMotionPauseListener(isPaused -> {
+                    if (isPaused && mState == STATE_ASSISTANT_ACTIVE) {
+                        mTimeFraction = 1;
+                        updateAssistantProgress();
+                    }
+                });
                 break;
             }
             case ACTION_POINTER_UP: {
@@ -107,94 +132,100 @@
                 break;
             }
             case ACTION_MOVE: {
+                if (mState == STATE_DELEGATE_ACTIVE) {
+                    break;
+                }
                 int pointerIndex = ev.findPointerIndex(mActivePointerId);
                 if (pointerIndex == -1) {
                     break;
                 }
                 mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
-                float displacement = getDisplacement(ev);
 
-                if (!mPassedDragSlop) {
-                    // Normal gesture, ensure we pass the drag slop before we start tracking
-                    // the gesture
-                    if (Math.abs(displacement) > mDragSlop) {
-                        mPassedDragSlop = true;
-                        mStartDisplacement = displacement;
-                        mPassedTouchSlopTime = SystemClock.uptimeMillis();
-                    }
-                }
+                if (!mPassedSlop) {
+                    // Normal gesture, ensure we pass the slop before we start tracking the gesture
+                    if (Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) > mSlop) {
+                        mPassedSlop = true;
+                        mStartDragPos.set(mLastPos.x, mLastPos.y);
+                        mDragTime = SystemClock.uptimeMillis();
 
-                if (!mPassedTouchSlop) {
-                    if (Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) >=
-                        mTouchSlop) {
-                        mPassedTouchSlop = true;
-                        if (!mPassedDragSlop) {
-                            mPassedDragSlop = true;
-                            mStartDisplacement = displacement;
-                            mPassedTouchSlopTime = SystemClock.uptimeMillis();
+                        // Determine if angle is larger than threshold for assistant detection
+                        float angle = (float) Math.toDegrees(
+                                Math.atan2(mDownPos.y - mLastPos.y, mDownPos.x - mLastPos.x));
+                        angle = angle > 90 ? 180 - angle : angle;
+                        if (angle > mAngleThreshold) {
+                            mState = STATE_ASSISTANT_ACTIVE;
+
+                            if (mConsumerDelegate != null) {
+                                // Send cancel event
+                                MotionEvent event = MotionEvent.obtain(ev);
+                                event.setAction(MotionEvent.ACTION_CANCEL);
+                                mConsumerDelegate.onMotionEvent(event);
+                            }
+                        } else {
+                            mState = STATE_DELEGATE_ACTIVE;
                         }
                     }
-                }
-
-                if (mPassedDragSlop) {
-                    // Move
-                    float distance = mStartDisplacement - displacement;
-                    if (distance >= 0) {
-                        onAssistantProgress(distance / mThreshold);
+                } else {
+                    // Movement
+                    mDistance = (float) Math.hypot(mLastPos.x - mStartDragPos.x,
+                            mLastPos.y - mStartDragPos.y);
+                    mMotionPauseDetector.addPosition(mDistance, 0);
+                    if (mDistance >= 0) {
+                        final long diff = SystemClock.uptimeMillis() - mDragTime;
+                        mTimeFraction = Math.min(diff * 1f / mTimeThreshold, 1);
+                        updateAssistantProgress();
                     }
                 }
                 break;
             }
             case ACTION_CANCEL:
-                break;
-            case ACTION_UP: {
-                if (ev.getEventTime() - mPassedTouchSlopTime < ViewConfiguration.getTapTimeout()) {
-                    onAssistantProgress(1);
+            case ACTION_UP:
+                if (mState != STATE_DELEGATE_ACTIVE && !mLaunchedAssistant) {
+                    ValueAnimator animator = ValueAnimator.ofFloat(mLastProgress, 0)
+                            .setDuration(RETRACT_ANIMATION_DURATION_MS);
+                    animator.addUpdateListener(valueAnimator -> {
+                            float progress = (float) valueAnimator.getAnimatedValue();
+                            try {
+                                mSysUiProxy.onAssistantProgress(progress);
+                            } catch (RemoteException e) {
+                                Log.w(TAG, "Failed to send SysUI start/send assistant progress: "
+                                        + progress, e);
+                            }
+                    });
+                    animator.setInterpolator(Interpolators.DEACCEL_2);
+                    animator.start();
                 }
-
+                mMotionPauseDetector.clear();
                 break;
-            }
+        }
+
+        if (mState != STATE_ASSISTANT_ACTIVE && mConsumerDelegate != null) {
+            mConsumerDelegate.onMotionEvent(ev);
         }
     }
 
-    private void onAssistantProgress(float progress) {
-        if (mLastProgress == progress) {
-            return;
-        }
-        try {
-            mSysUiProxy.onAssistantProgress(Math.max(0, Math.min(1, progress)));
-            if (progress >= 1 && !mLaunchedAssistant) {
-                mSysUiProxy.startAssistant(new Bundle());
-                mLaunchedAssistant = true;
-            }
+    private void updateAssistantProgress() {
+        if (!mLaunchedAssistant) {
+            float progress = Math.min(mDistance * 1f / mDistThreshold, 1) * mTimeFraction;
             mLastProgress = progress;
-        } catch (RemoteException e) {
-            Log.w(TAG, "Failed to notify SysUI to start/send assistant progress: " + progress, e);
+            try {
+                mSysUiProxy.onAssistantProgress(progress);
+
+                if (mDistance >= mDistThreshold && mTimeFraction >= 1) {
+                    mSysUiProxy.startAssistant(new Bundle());
+                    mLaunchedAssistant = true;
+                }
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed to send SysUI start/send assistant progress: " + progress, e);
+            }
         }
     }
 
-    private boolean isNavBarOnRight() {
-        return mDisplayRotation == Surface.ROTATION_90 && mStableInsets.right > 0;
-    }
-
-    private boolean isNavBarOnLeft() {
-        return mDisplayRotation == Surface.ROTATION_270 && mStableInsets.left > 0;
-    }
-
-    private float getDisplacement(MotionEvent ev) {
-        float eventX = ev.getX();
-        float eventY = ev.getY();
-        float displacement = eventY - mDownPos.y;
-        if (isNavBarOnRight()) {
-            displacement = eventX - mDownPos.x;
-        } else if (isNavBarOnLeft()) {
-            displacement = mDownPos.x - eventX;
-        }
-        return displacement;
-    }
-
-    static boolean withinTouchRegion(Context context, float x) {
-        return x > context.getResources().getDisplayMetrics().widthPixels
-                - context.getResources().getDimension(R.dimen.gestures_assistant_width);
+    static boolean withinTouchRegion(Context context, MotionEvent ev) {
+        final Resources res = context.getResources();
+        final int width = res.getDisplayMetrics().widthPixels;
+        final int height = res.getDisplayMetrics().heightPixels;
+        final int size = res.getDimensionPixelSize(R.dimen.gestures_assistant_size);
+        return (ev.getX() > width - size || ev.getX() < size) && ev.getY() > height - size;
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index c281d2c..8fe0461 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -311,31 +311,36 @@
             mSwipeSharedState.clearAllState();
         }
 
+        final ActivityControlHelper activityControl =
+                mOverviewComponentObserver.getActivityControlHelper();
         if (runningTaskInfo == null && !mSwipeSharedState.goingToLauncher) {
             return InputConsumer.NO_OP;
         } else if (mAssistantAvailable && mOverviewInteractionState.isSwipeUpGestureEnabled()
                 && FeatureFlags.ENABLE_ASSISTANT_GESTURE.get()
-                && AssistantTouchConsumer.withinTouchRegion(this, event.getX())) {
-            return new AssistantTouchConsumer(this, mRecentsModel.getSystemUiProxy());
-        } else if (mSwipeSharedState.goingToLauncher ||
-                mOverviewComponentObserver.getActivityControlHelper().isResumed()) {
-            return OverviewInputConsumer.newInstance(
-                    mOverviewComponentObserver.getActivityControlHelper(), false);
+                && AssistantTouchConsumer.withinTouchRegion(this, event)) {
+            return new AssistantTouchConsumer(this, mISystemUiProxy, !activityControl.isResumed()
+                            ? createOtherActivityInputConsumer(event, runningTaskInfo) : null);
+        } else if (mSwipeSharedState.goingToLauncher || activityControl.isResumed()) {
+            return OverviewInputConsumer.newInstance(activityControl, false);
         } else if (ENABLE_QUICKSTEP_LIVE_TILE.get() &&
-                mOverviewComponentObserver.getActivityControlHelper().isInLiveTileMode()) {
-            return OverviewInputConsumer.newInstance(
-                    mOverviewComponentObserver.getActivityControlHelper(), false);
+                activityControl.isInLiveTileMode()) {
+            return OverviewInputConsumer.newInstance(activityControl, false);
         } else {
-            ActivityControlHelper activityControl =
-                    mOverviewComponentObserver.getActivityControlHelper();
-            boolean shouldDefer = activityControl.deferStartingActivity(mActiveNavBarRegion, event);
-            return new OtherActivityInputConsumer(this, runningTaskInfo, mRecentsModel,
-                    mOverviewComponentObserver.getOverviewIntent(), activityControl,
-                    shouldDefer, mOverviewCallbacks, mTaskOverlayFactory, mInputConsumer,
-                    this::onConsumerInactive, mSwipeSharedState);
+            return createOtherActivityInputConsumer(event, runningTaskInfo);
         }
     }
 
+    private OtherActivityInputConsumer createOtherActivityInputConsumer(MotionEvent event,
+            RunningTaskInfo runningTaskInfo) {
+        final ActivityControlHelper activityControl =
+                mOverviewComponentObserver.getActivityControlHelper();
+        boolean shouldDefer = activityControl.deferStartingActivity(mActiveNavBarRegion, event);
+        return new OtherActivityInputConsumer(this, runningTaskInfo, mRecentsModel,
+                mOverviewComponentObserver.getOverviewIntent(), activityControl,
+                shouldDefer, mOverviewCallbacks, mTaskOverlayFactory, mInputConsumer,
+                this::onConsumerInactive, mSwipeSharedState);
+    }
+
     /**
      * To be called by the consumer when it's no longer active.
      */
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 3fbfcdd..a966698 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -26,4 +26,8 @@
          determines how many thumbnails will be fetched in the background. -->
     <integer name="recentsThumbnailCacheSize">3</integer>
     <integer name="recentsIconCacheSize">12</integer>
+
+    <!-- Assistant Gesture -->
+    <integer name="assistant_gesture_min_time_threshold">200</integer>
+    <integer name="assistant_gesture_corner_deg_threshold">30</integer>
 </resources>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index f5e5dd3..9c97c8c 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -66,6 +66,6 @@
     <dimen name="shelf_surface_offset">24dp</dimen>
 
     <!-- Assistant Gestures -->
-    <dimen name="gestures_assistant_width">70dp</dimen>
-    <dimen name="gestures_assistant_threshold">200dp</dimen>
+    <dimen name="gestures_assistant_size">28dp</dimen>
+    <dimen name="gestures_assistant_drag_threshold">70dp</dimen>
 </resources>