Adds Assistant Sandbox tutorial.

Demo: https://drive.google.com/open?id=1onivF9qKdgJeUG2ROkQBxcgi-l0VfC4f

Fixes: 157824552

Change-Id: Ia0e5f46b39e3f06feed2f7e175ab7861e9d51a96
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 77345a0..39a2a32 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -132,6 +132,17 @@
     <!-- Feedback shown during interactive parts of Overview gesture tutorial when the gesture is horizontal instead of vertical. [CHAR LIMIT=100] -->
     <string name="overview_gesture_feedback_wrong_swipe_direction" translatable="false">Make sure you swipe straight up and pause</string>
 
+    <!-- Title shown during interactive part of Assistant gesture tutorial. [CHAR LIMIT=30] -->
+    <string name="assistant_gesture_tutorial_playground_title" translatable="false">Tutorial: Assistant</string>
+    <!-- Subtitle shown during interactive parts of Assistant gesture tutorial. [CHAR LIMIT=60] -->
+    <string name="assistant_gesture_tutorial_playground_subtitle" translatable="false">Try swiping diagonally from a bottom corner of the screen</string>
+    <!-- Feedback shown during interactive parts of Assistant gesture tutorial when the gesture is started too far from the corner. [CHAR LIMIT=100] -->
+    <string name="assistant_gesture_feedback_swipe_too_far_from_corner" translatable="false">Make sure you swipe from a bottom corner of the screen</string>
+    <!-- Feedback shown during interactive parts of Assistant gesture tutorial when the gesture doesn't go diagonally enough. [CHAR LIMIT=100] -->
+    <string name="assistant_gesture_feedback_swipe_not_diagonal" translatable="false">Make sure you swipe diagonally</string>
+    <!-- Feedback shown during interactive parts of Assistant gesture tutorial when the gesture doesn't go far enough. [CHAR LIMIT=100] -->
+    <string name="assistant_gesture_feedback_swipe_not_long_enough" translatable="false">Try swiping further</string>
+
     <!-- Title shown on the confirmation screen after successful gesture. [CHAR LIMIT=30] -->
     <string name="gesture_tutorial_confirm_title" translatable="false">All set</string>
     <!-- Button text shown on a button on the confirm screen to leave the tutorial. [CHAR LIMIT=14] -->
diff --git a/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java
new file mode 100644
index 0000000..6862f07
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import static com.android.quickstep.interaction.TutorialController.TutorialType.ASSISTANT_COMPLETE;
+
+import android.graphics.PointF;
+import android.view.View;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
+import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
+
+/** A {@link TutorialController} for the Assistant tutorial. */
+final class AssistantGestureTutorialController extends TutorialController {
+
+    AssistantGestureTutorialController(AssistantGestureTutorialFragment fragment,
+                                       TutorialType tutorialType) {
+        super(fragment, tutorialType);
+    }
+
+    @Override
+    Integer getTitleStringId() {
+        switch (mTutorialType) {
+            case ASSISTANT:
+                return R.string.assistant_gesture_tutorial_playground_title;
+            case ASSISTANT_COMPLETE:
+                return R.string.gesture_tutorial_confirm_title;
+        }
+        return null;
+    }
+
+    @Override
+    Integer getSubtitleStringId() {
+        if (mTutorialType == TutorialType.ASSISTANT) {
+            return R.string.assistant_gesture_tutorial_playground_subtitle;
+        }
+        return null;
+    }
+
+    @Override
+    Integer getActionButtonStringId() {
+        if (mTutorialType == ASSISTANT_COMPLETE) {
+            return R.string.gesture_tutorial_action_button_label_done;
+        }
+        return null;
+    }
+
+    @Override
+    void onActionButtonClicked(View button) {
+        mTutorialFragment.closeTutorial();
+    }
+
+    @Override
+    public void onBackGestureAttempted(BackGestureResult result) {
+        switch (mTutorialType) {
+            case ASSISTANT:
+                switch (result) {
+                    case BACK_COMPLETED_FROM_LEFT:
+                    case BACK_COMPLETED_FROM_RIGHT:
+                    case BACK_CANCELLED_FROM_LEFT:
+                    case BACK_CANCELLED_FROM_RIGHT:
+                        showFeedback(R.string.assistant_gesture_feedback_swipe_too_far_from_corner);
+                        break;
+                }
+                break;
+            case ASSISTANT_COMPLETE:
+                if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
+                        || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
+                    mTutorialFragment.closeTutorial();
+                }
+                break;
+        }
+    }
+
+
+    @Override
+    public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
+        switch (mTutorialType) {
+            case ASSISTANT:
+                switch (result) {
+                    case HOME_GESTURE_COMPLETED:
+                    case OVERVIEW_GESTURE_COMPLETED:
+                    case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE:
+                    case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE:
+                    case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
+                    case HOME_OR_OVERVIEW_CANCELLED:
+                        showFeedback(R.string.assistant_gesture_feedback_swipe_too_far_from_corner);
+                        break;
+                    case ASSISTANT_COMPLETED:
+                        hideFeedback();
+                        hideHandCoachingAnimation();
+                        showRippleEffect(
+                                () -> mTutorialFragment.changeController(ASSISTANT_COMPLETE));
+                        break;
+                    case ASSISTANT_NOT_STARTED_BAD_ANGLE:
+                        showFeedback(R.string.assistant_gesture_feedback_swipe_not_diagonal);
+                        break;
+                    case ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT:
+                        showFeedback(R.string.assistant_gesture_feedback_swipe_not_long_enough);
+                        break;
+                }
+                break;
+            case ASSISTANT_COMPLETE:
+                if (result == NavBarGestureResult.HOME_GESTURE_COMPLETED) {
+                    mTutorialFragment.closeTutorial();
+                }
+                break;
+        }
+    }
+
+    @Override
+    public void setAssistantProgress(float progress) {
+        // TODO: Create an animation.
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java
new file mode 100644
index 0000000..70181fb
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.TutorialController.TutorialType;
+
+/** Shows the Home gesture interactive tutorial. */
+public class AssistantGestureTutorialFragment extends TutorialFragment {
+    @Override
+    int getHandAnimationResId() {
+        return R.drawable.assistant_gesture;
+    }
+
+    @Override
+    TutorialController createController(TutorialType type) {
+        return new AssistantGestureTutorialController(this, type);
+    }
+
+    @Override
+    Class<? extends TutorialController> getControllerClass() {
+        return AssistantGestureTutorialController.class;
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent motionEvent) {
+        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN && mTutorialController != null) {
+            mTutorialController.setRippleHotspot(motionEvent.getX(), motionEvent.getY());
+        }
+        return super.onTouch(view, motionEvent);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
index 1f398fc..921e568 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -21,8 +21,6 @@
 import android.graphics.PointF;
 import android.view.View;
 
-import androidx.annotation.Nullable;
-
 import com.android.launcher3.R;
 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
@@ -156,7 +154,4 @@
             }
         }
     }
-
-    @Override
-    public void setNavBarGestureProgress(@Nullable Float displacement) {}
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
index 4069c09..0e2312b 100644
--- a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
+++ b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
@@ -15,6 +15,10 @@
  */
 package com.android.quickstep.interaction;
 
+import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_COMPLETED;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_BAD_ANGLE;
+import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_GESTURE_COMPLETED;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_NOT_STARTED_TOO_FAR_FROM_EDGE;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_CANCELLED;
@@ -22,38 +26,69 @@
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_GESTURE_COMPLETED;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE;
 
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.SystemClock;
 import android.view.Display;
+import android.view.GestureDetector;
 import android.view.MotionEvent;
 import android.view.Surface;
 import android.view.View;
 import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.R;
 import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.util.VibratorWrapper;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.util.NavBarPosition;
 import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
+import com.android.systemui.shared.system.QuickStepContract;
 
-/** Utility class to handle home gestures. */
+/** Utility class to handle Home and Assistant gestures. */
 public class NavBarGestureHandler implements OnTouchListener,
         TriggerSwipeUpTouchTracker.OnSwipeUpListener {
 
     private static final String LOG_TAG = "NavBarGestureHandler";
+    private static final long RETRACT_GESTURE_ANIMATION_DURATION_MS = 300;
 
+    private final Context mContext;
     private final Point mDisplaySize = new Point();
     private final TriggerSwipeUpTouchTracker mSwipeUpTouchTracker;
-    private int mBottomGestureHeight;
+    private final int mBottomGestureHeight;
+    private final GestureDetector mAssistantGestureDetector;
+    private final int mAssistantAngleThreshold;
+    private final RectF mAssistantLeftRegion = new RectF();
+    private final RectF mAssistantRightRegion = new RectF();
+    private final float mAssistantDragDistThreshold;
+    private final float mAssistantFlingDistThreshold;
+    private final long mAssistantTimeThreshold;
+    private final float mAssistantSquaredSlop;
+    private final PointF mAssistantStartDragPos = new PointF();
+    private final PointF mDownPos = new PointF();
+    private final PointF mLastPos = new PointF();
+    private boolean mTouchCameFromAssistantCorner;
     private boolean mTouchCameFromNavBar;
-    private float mDownY;
+    private boolean mPassedAssistantSlop;
+    private boolean mAssistantGestureActive;
+    private boolean mLaunchedAssistant;
+    private long mAssistantDragStartTime;
+    private float mAssistantDistance;
+    private float mAssistantTimeFraction;
+    private float mAssistantLastProgress;
+    @Nullable
     private NavBarGestureAttemptCallback mGestureCallback;
 
     NavBarGestureHandler(Context context) {
-        final Display display = context.getDisplay();
+        mContext = context;
+        final Display display = mContext.getDisplay();
         final int displayRotation;
         if (display == null) {
             displayRotation = Surface.ROTATION_0;
@@ -61,7 +96,6 @@
             displayRotation = display.getRotation();
             display.getRealSize(mDisplaySize);
         }
-        mDownY = mDisplaySize.y;
         mSwipeUpTouchTracker =
                 new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/,
                         new NavBarPosition(Mode.NO_BUTTON, displayRotation),
@@ -70,6 +104,27 @@
         final Resources resources = context.getResources();
         mBottomGestureHeight =
                 ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, resources);
+        mAssistantDragDistThreshold =
+                resources.getDimension(R.dimen.gestures_assistant_drag_threshold);
+        mAssistantFlingDistThreshold =
+                resources.getDimension(R.dimen.gestures_assistant_fling_threshold);
+        mAssistantTimeThreshold =
+                resources.getInteger(R.integer.assistant_gesture_min_time_threshold);
+        mAssistantAngleThreshold =
+                resources.getInteger(R.integer.assistant_gesture_corner_deg_threshold);
+
+        mAssistantGestureDetector = new GestureDetector(context, new AssistantGestureListener());
+        int assistantWidth = resources.getDimensionPixelSize(R.dimen.gestures_assistant_width);
+        final float assistantHeight = Math.max(mBottomGestureHeight,
+                QuickStepContract.getWindowCornerRadius(resources));
+        mAssistantLeftRegion.bottom = mAssistantRightRegion.bottom = mDisplaySize.y;
+        mAssistantLeftRegion.top = mAssistantRightRegion.top = mDisplaySize.y - assistantHeight;
+        mAssistantLeftRegion.left = 0;
+        mAssistantLeftRegion.right = assistantWidth;
+        mAssistantRightRegion.right = mDisplaySize.x;
+        mAssistantRightRegion.left = mDisplaySize.x - assistantWidth;
+        float slop = ViewConfiguration.get(context).getScaledTouchSlop();
+        mAssistantSquaredSlop = slop * slop;
     }
 
     void registerNavBarGestureAttemptCallback(NavBarGestureAttemptCallback callback) {
@@ -82,7 +137,7 @@
 
     @Override
     public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
-        if (mGestureCallback == null) {
+        if (mGestureCallback == null || mAssistantGestureActive) {
             return;
         }
         finalVelocity.set(finalVelocity.x / 1000, finalVelocity.y / 1000);
@@ -98,36 +153,128 @@
 
     @Override
     public void onSwipeUpCancelled() {
-        if (mGestureCallback != null) {
+        if (mGestureCallback != null && !mAssistantGestureActive) {
             mGestureCallback.onNavBarGestureAttempted(HOME_OR_OVERVIEW_CANCELLED, new PointF());
         }
     }
 
     @Override
-    public boolean onTouch(View view, MotionEvent motionEvent) {
-        int action = motionEvent.getAction();
+    public boolean onTouch(View view, MotionEvent event) {
+        int action = event.getAction();
         boolean intercepted = mSwipeUpTouchTracker.interceptedTouch();
-        if (action == MotionEvent.ACTION_DOWN) {
-            mDownY = motionEvent.getY();
-            mTouchCameFromNavBar = mDownY >= mDisplaySize.y - mBottomGestureHeight;
-            if (!mTouchCameFromNavBar) {
-                mGestureCallback.setNavBarGestureProgress(null);
-            }
-            mSwipeUpTouchTracker.init();
-        } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
-            if (mGestureCallback != null && !intercepted && mTouchCameFromNavBar) {
-                mGestureCallback.onNavBarGestureAttempted(
-                        HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION, new PointF());
-                intercepted = true;
-            }
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                mDownPos.set(event.getX(), event.getY());
+                mLastPos.set(mDownPos);
+                mTouchCameFromAssistantCorner =
+                        mAssistantLeftRegion.contains(event.getX(), event.getY())
+                                || mAssistantRightRegion.contains(event.getX(), event.getY());
+                mAssistantGestureActive = mTouchCameFromAssistantCorner;
+                mTouchCameFromNavBar = !mTouchCameFromAssistantCorner
+                        && mDownPos.y >= mDisplaySize.y - mBottomGestureHeight;
+                if (!mTouchCameFromNavBar && mGestureCallback != null) {
+                    mGestureCallback.setNavBarGestureProgress(null);
+                }
+                mLaunchedAssistant = false;
+                mSwipeUpTouchTracker.init();
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (!mAssistantGestureActive) {
+                    break;
+                }
+                mLastPos.set(event.getX(), event.getY());
+
+                if (!mPassedAssistantSlop) {
+                    // Normal gesture, ensure we pass the slop before we start tracking the gesture
+                    if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
+                            > mAssistantSquaredSlop) {
+
+                        mPassedAssistantSlop = true;
+                        mAssistantStartDragPos.set(mLastPos.x, mLastPos.y);
+                        mAssistantDragStartTime = SystemClock.uptimeMillis();
+
+                        mAssistantGestureActive = isValidAssistantGestureAngle(
+                                mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y);
+                        if (!mAssistantGestureActive && mGestureCallback != null) {
+                            mGestureCallback.onNavBarGestureAttempted(
+                                    ASSISTANT_NOT_STARTED_BAD_ANGLE, new PointF());
+                        }
+                    }
+                } else {
+                    // Movement
+                    mAssistantDistance = (float) Math.hypot(mLastPos.x - mAssistantStartDragPos.x,
+                            mLastPos.y - mAssistantStartDragPos.y);
+                    if (mAssistantDistance >= 0) {
+                        final long diff = SystemClock.uptimeMillis() - mAssistantDragStartTime;
+                        mAssistantTimeFraction = Math.min(diff * 1f / mAssistantTimeThreshold, 1);
+                        updateAssistantProgress();
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                if (mGestureCallback != null && !intercepted && mTouchCameFromNavBar) {
+                    mGestureCallback.onNavBarGestureAttempted(
+                            HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION, new PointF());
+                    intercepted = true;
+                    break;
+                }
+                if (mAssistantGestureActive && !mLaunchedAssistant && mGestureCallback != null) {
+                    mGestureCallback.onNavBarGestureAttempted(
+                            ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT, new PointF());
+                    ValueAnimator animator = ValueAnimator.ofFloat(mAssistantLastProgress, 0)
+                            .setDuration(RETRACT_GESTURE_ANIMATION_DURATION_MS);
+                    animator.addUpdateListener(valueAnimator -> {
+                        float progress = (float) valueAnimator.getAnimatedValue();
+                        mGestureCallback.setAssistantProgress(progress);
+                    });
+                    animator.setInterpolator(Interpolators.DEACCEL_2);
+                    animator.start();
+                }
+                mPassedAssistantSlop = false;
+                break;
         }
         if (mTouchCameFromNavBar && mGestureCallback != null) {
-            mGestureCallback.setNavBarGestureProgress(motionEvent.getY() - mDownY);
+            mGestureCallback.setNavBarGestureProgress(event.getY() - mDownPos.y);
         }
-        mSwipeUpTouchTracker.onMotionEvent(motionEvent);
+        mSwipeUpTouchTracker.onMotionEvent(event);
+        mAssistantGestureDetector.onTouchEvent(event);
         return intercepted;
     }
 
+    /**
+     * Determine if angle is larger than threshold for assistant detection
+     */
+    private boolean isValidAssistantGestureAngle(float deltaX, float deltaY) {
+        float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+
+        // normalize so that angle is measured clockwise from horizontal in the bottom right corner
+        // and counterclockwise from horizontal in the bottom left corner
+        angle = angle > 90 ? 180 - angle : angle;
+        return (angle > mAssistantAngleThreshold && angle < 90);
+    }
+
+    private void updateAssistantProgress() {
+        if (!mLaunchedAssistant) {
+            mAssistantLastProgress =
+                    Math.min(mAssistantDistance * 1f / mAssistantDragDistThreshold, 1)
+                            * mAssistantTimeFraction;
+            if (mAssistantDistance >= mAssistantDragDistThreshold && mAssistantTimeFraction >= 1) {
+                startAssistant(new PointF());
+            } else if (mGestureCallback != null) {
+                mGestureCallback.setAssistantProgress(mAssistantLastProgress);
+            }
+        }
+    }
+
+    private void startAssistant(PointF velocity) {
+        if (mGestureCallback != null) {
+            mGestureCallback.onNavBarGestureAttempted(ASSISTANT_COMPLETED, velocity);
+        }
+        VibratorWrapper.INSTANCE.get(mContext).vibrate(VibratorWrapper.EFFECT_CLICK);
+        mLaunchedAssistant = true;
+    }
+
     enum NavBarGestureResult {
         UNKNOWN,
         HOME_GESTURE_COMPLETED,
@@ -135,7 +282,10 @@
         HOME_NOT_STARTED_TOO_FAR_FROM_EDGE,
         OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE,
         HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION,  // Side swipe on nav bar.
-        HOME_OR_OVERVIEW_CANCELLED
+        HOME_OR_OVERVIEW_CANCELLED,
+        ASSISTANT_COMPLETED,
+        ASSISTANT_NOT_STARTED_BAD_ANGLE,
+        ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT,
     }
 
     /** Callback to let the UI react to attempted nav bar gestures. */
@@ -144,6 +294,28 @@
         void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity);
 
         /** Indicates how far a touch originating in the nav bar has moved from the nav bar. */
-        void setNavBarGestureProgress(@Nullable Float displacement);
+        default void setNavBarGestureProgress(@Nullable Float displacement) {}
+
+        /** Indicates the progress of an Assistant gesture. */
+        default void setAssistantProgress(float progress) {}
+    }
+
+    private class AssistantGestureListener extends GestureDetector.SimpleOnGestureListener {
+        @Override
+        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+            if (!mLaunchedAssistant && mTouchCameFromAssistantCorner) {
+                PointF velocity = new PointF(velocityX, velocityY);
+                if (!isValidAssistantGestureAngle(velocityX, -velocityY)) {
+                    if (mGestureCallback != null) {
+                        mGestureCallback.onNavBarGestureAttempted(ASSISTANT_NOT_STARTED_BAD_ANGLE,
+                                velocity);
+                    }
+                } else if (mAssistantDistance >= mAssistantFlingDistThreshold) {
+                    mAssistantLastProgress = 1;
+                    startAssistant(velocity);
+                }
+            }
+            return true;
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index 511c8b6..c1918c2 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -202,7 +202,8 @@
     private boolean isComplete() {
         return mTutorialType == TutorialType.BACK_NAVIGATION_COMPLETE
                 || mTutorialType == TutorialType.HOME_NAVIGATION_COMPLETE
-                || mTutorialType == TutorialType.OVERVIEW_NAVIGATION_COMPLETE;
+                || mTutorialType == TutorialType.OVERVIEW_NAVIGATION_COMPLETE
+                || mTutorialType == TutorialType.ASSISTANT_COMPLETE;
     }
 
     /** Denotes the type of the tutorial. */
@@ -213,6 +214,8 @@
         HOME_NAVIGATION,
         HOME_NAVIGATION_COMPLETE,
         OVERVIEW_NAVIGATION,
-        OVERVIEW_NAVIGATION_COMPLETE
+        OVERVIEW_NAVIGATION_COMPLETE,
+        ASSISTANT,
+        ASSISTANT_COMPLETE
     }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
index da6815d..9a8264d 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
@@ -71,6 +71,9 @@
             case OVERVIEW_NAVIGATION:
             case OVERVIEW_NAVIGATION_COMPLETE:
                 return new OverviewGestureTutorialFragment();
+            case ASSISTANT:
+            case ASSISTANT_COMPLETE:
+                return new AssistantGestureTutorialFragment();
             default:
                 Log.e(LOG_TAG, "Failed to find an appropriate fragment for " + tutorialType.name());
         }