Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ad6a4dce authored by Andy Wickham's avatar Andy Wickham Committed by Android (Google) Code Review
Browse files

Merge "Adds Assistant Sandbox tutorial." into ub-launcher3-rvc-dev

parents f2a6d5b7 1a5795d1
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -134,6 +134,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] -->
+129 −0
Original line number Diff line number Diff line
/*
 * 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.
    }
}
+48 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+0 −5
Original line number Diff line number Diff line
@@ -21,8 +21,6 @@ import static com.android.quickstep.interaction.TutorialController.TutorialType.
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 @@ final class BackGestureTutorialController extends TutorialController {
            }
        }
    }

    @Override
    public void setNavBarGestureProgress(@Nullable Float displacement) {}
}
+198 −26
Original line number Diff line number Diff line
@@ -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.NavBarGestu
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 @@ public class NavBarGestureHandler implements OnTouchListener,
            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 @@ public class NavBarGestureHandler implements OnTouchListener,
        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 @@ public class NavBarGestureHandler implements OnTouchListener,

    @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 @@ public class NavBarGestureHandler implements OnTouchListener,

    @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) {
        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();
        } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
                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 @@ public class NavBarGestureHandler implements OnTouchListener,
        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 @@ public class NavBarGestureHandler implements OnTouchListener,
        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;
        }
    }
}
Loading