1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.quickstep.interaction;
17 
18 import static com.android.launcher3.Utilities.squaredHypot;
19 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_COMPLETED;
20 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_BAD_ANGLE;
21 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT;
22 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_GESTURE_COMPLETED;
23 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_NOT_STARTED_TOO_FAR_FROM_EDGE;
24 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_CANCELLED;
25 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION;
26 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_GESTURE_COMPLETED;
27 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE;
28 
29 import android.animation.ValueAnimator;
30 import android.content.Context;
31 import android.content.res.Resources;
32 import android.graphics.Point;
33 import android.graphics.PointF;
34 import android.graphics.RectF;
35 import android.os.SystemClock;
36 import android.view.Display;
37 import android.view.GestureDetector;
38 import android.view.MotionEvent;
39 import android.view.Surface;
40 import android.view.View;
41 import android.view.View.OnTouchListener;
42 import android.view.ViewConfiguration;
43 
44 import androidx.annotation.Nullable;
45 
46 import com.android.launcher3.R;
47 import com.android.launcher3.ResourceUtils;
48 import com.android.launcher3.anim.Interpolators;
49 import com.android.launcher3.util.VibratorWrapper;
50 import com.android.quickstep.SysUINavigationMode.Mode;
51 import com.android.quickstep.util.NavBarPosition;
52 import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
53 import com.android.systemui.shared.system.QuickStepContract;
54 
55 /** Utility class to handle Home and Assistant gestures. */
56 public class NavBarGestureHandler implements OnTouchListener,
57         TriggerSwipeUpTouchTracker.OnSwipeUpListener {
58 
59     private static final String LOG_TAG = "NavBarGestureHandler";
60     private static final long RETRACT_GESTURE_ANIMATION_DURATION_MS = 300;
61 
62     private final Context mContext;
63     private final Point mDisplaySize = new Point();
64     private final TriggerSwipeUpTouchTracker mSwipeUpTouchTracker;
65     private final int mBottomGestureHeight;
66     private final GestureDetector mAssistantGestureDetector;
67     private final int mAssistantAngleThreshold;
68     private final RectF mAssistantLeftRegion = new RectF();
69     private final RectF mAssistantRightRegion = new RectF();
70     private final float mAssistantDragDistThreshold;
71     private final float mAssistantFlingDistThreshold;
72     private final long mAssistantTimeThreshold;
73     private final float mAssistantSquaredSlop;
74     private final PointF mAssistantStartDragPos = new PointF();
75     private final PointF mDownPos = new PointF();
76     private final PointF mLastPos = new PointF();
77     private boolean mTouchCameFromAssistantCorner;
78     private boolean mTouchCameFromNavBar;
79     private boolean mPassedAssistantSlop;
80     private boolean mAssistantGestureActive;
81     private boolean mLaunchedAssistant;
82     private long mAssistantDragStartTime;
83     private float mAssistantDistance;
84     private float mAssistantTimeFraction;
85     private float mAssistantLastProgress;
86     @Nullable
87     private NavBarGestureAttemptCallback mGestureCallback;
88 
NavBarGestureHandler(Context context)89     NavBarGestureHandler(Context context) {
90         mContext = context;
91         final Display display = mContext.getDisplay();
92         final int displayRotation;
93         if (display == null) {
94             displayRotation = Surface.ROTATION_0;
95         } else {
96             displayRotation = display.getRotation();
97             display.getRealSize(mDisplaySize);
98         }
99         mSwipeUpTouchTracker =
100                 new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/,
101                         new NavBarPosition(Mode.NO_BUTTON, displayRotation),
102                         null /*onInterceptTouch*/, this);
103 
104         final Resources resources = context.getResources();
105         mBottomGestureHeight =
106                 ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, resources);
107         mAssistantDragDistThreshold =
108                 resources.getDimension(R.dimen.gestures_assistant_drag_threshold);
109         mAssistantFlingDistThreshold =
110                 resources.getDimension(R.dimen.gestures_assistant_fling_threshold);
111         mAssistantTimeThreshold =
112                 resources.getInteger(R.integer.assistant_gesture_min_time_threshold);
113         mAssistantAngleThreshold =
114                 resources.getInteger(R.integer.assistant_gesture_corner_deg_threshold);
115 
116         mAssistantGestureDetector = new GestureDetector(context, new AssistantGestureListener());
117         int assistantWidth = resources.getDimensionPixelSize(R.dimen.gestures_assistant_width);
118         final float assistantHeight = Math.max(mBottomGestureHeight,
119                 QuickStepContract.getWindowCornerRadius(resources));
120         mAssistantLeftRegion.bottom = mAssistantRightRegion.bottom = mDisplaySize.y;
121         mAssistantLeftRegion.top = mAssistantRightRegion.top = mDisplaySize.y - assistantHeight;
122         mAssistantLeftRegion.left = 0;
123         mAssistantLeftRegion.right = assistantWidth;
124         mAssistantRightRegion.right = mDisplaySize.x;
125         mAssistantRightRegion.left = mDisplaySize.x - assistantWidth;
126         float slop = ViewConfiguration.get(context).getScaledTouchSlop();
127         mAssistantSquaredSlop = slop * slop;
128     }
129 
registerNavBarGestureAttemptCallback(NavBarGestureAttemptCallback callback)130     void registerNavBarGestureAttemptCallback(NavBarGestureAttemptCallback callback) {
131         mGestureCallback = callback;
132     }
133 
unregisterNavBarGestureAttemptCallback()134     void unregisterNavBarGestureAttemptCallback() {
135         mGestureCallback = null;
136     }
137 
138     @Override
onSwipeUp(boolean wasFling, PointF finalVelocity)139     public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
140         if (mGestureCallback == null || mAssistantGestureActive) {
141             return;
142         }
143         finalVelocity.set(finalVelocity.x / 1000, finalVelocity.y / 1000);
144         if (mTouchCameFromNavBar) {
145             mGestureCallback.onNavBarGestureAttempted(wasFling
146                     ? HOME_GESTURE_COMPLETED : OVERVIEW_GESTURE_COMPLETED, finalVelocity);
147         } else {
148             mGestureCallback.onNavBarGestureAttempted(wasFling
149                     ? HOME_NOT_STARTED_TOO_FAR_FROM_EDGE : OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE,
150                     finalVelocity);
151         }
152     }
153 
154     @Override
onSwipeUpCancelled()155     public void onSwipeUpCancelled() {
156         if (mGestureCallback != null && !mAssistantGestureActive) {
157             mGestureCallback.onNavBarGestureAttempted(HOME_OR_OVERVIEW_CANCELLED, new PointF());
158         }
159     }
160 
161     @Override
onTouch(View view, MotionEvent event)162     public boolean onTouch(View view, MotionEvent event) {
163         int action = event.getAction();
164         boolean intercepted = mSwipeUpTouchTracker.interceptedTouch();
165         switch (action) {
166             case MotionEvent.ACTION_DOWN:
167                 mDownPos.set(event.getX(), event.getY());
168                 mLastPos.set(mDownPos);
169                 mTouchCameFromAssistantCorner =
170                         mAssistantLeftRegion.contains(event.getX(), event.getY())
171                                 || mAssistantRightRegion.contains(event.getX(), event.getY());
172                 mAssistantGestureActive = mTouchCameFromAssistantCorner;
173                 mTouchCameFromNavBar = !mTouchCameFromAssistantCorner
174                         && mDownPos.y >= mDisplaySize.y - mBottomGestureHeight;
175                 if (!mTouchCameFromNavBar && mGestureCallback != null) {
176                     mGestureCallback.setNavBarGestureProgress(null);
177                 }
178                 mLaunchedAssistant = false;
179                 mSwipeUpTouchTracker.init();
180                 break;
181             case MotionEvent.ACTION_MOVE:
182                 if (!mAssistantGestureActive) {
183                     break;
184                 }
185                 mLastPos.set(event.getX(), event.getY());
186 
187                 if (!mPassedAssistantSlop) {
188                     // Normal gesture, ensure we pass the slop before we start tracking the gesture
189                     if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
190                             > mAssistantSquaredSlop) {
191 
192                         mPassedAssistantSlop = true;
193                         mAssistantStartDragPos.set(mLastPos.x, mLastPos.y);
194                         mAssistantDragStartTime = SystemClock.uptimeMillis();
195 
196                         mAssistantGestureActive = isValidAssistantGestureAngle(
197                                 mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y);
198                         if (!mAssistantGestureActive && mGestureCallback != null) {
199                             mGestureCallback.onNavBarGestureAttempted(
200                                     ASSISTANT_NOT_STARTED_BAD_ANGLE, new PointF());
201                         }
202                     }
203                 } else {
204                     // Movement
205                     mAssistantDistance = (float) Math.hypot(mLastPos.x - mAssistantStartDragPos.x,
206                             mLastPos.y - mAssistantStartDragPos.y);
207                     if (mAssistantDistance >= 0) {
208                         final long diff = SystemClock.uptimeMillis() - mAssistantDragStartTime;
209                         mAssistantTimeFraction = Math.min(diff * 1f / mAssistantTimeThreshold, 1);
210                         updateAssistantProgress();
211                     }
212                 }
213                 break;
214             case MotionEvent.ACTION_UP:
215             case MotionEvent.ACTION_CANCEL:
216                 if (mGestureCallback != null && !intercepted && mTouchCameFromNavBar) {
217                     mGestureCallback.onNavBarGestureAttempted(
218                             HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION, new PointF());
219                     intercepted = true;
220                     break;
221                 }
222                 if (mAssistantGestureActive && !mLaunchedAssistant && mGestureCallback != null) {
223                     mGestureCallback.onNavBarGestureAttempted(
224                             ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT, new PointF());
225                     ValueAnimator animator = ValueAnimator.ofFloat(mAssistantLastProgress, 0)
226                             .setDuration(RETRACT_GESTURE_ANIMATION_DURATION_MS);
227                     animator.addUpdateListener(valueAnimator -> {
228                         float progress = (float) valueAnimator.getAnimatedValue();
229                         mGestureCallback.setAssistantProgress(progress);
230                     });
231                     animator.setInterpolator(Interpolators.DEACCEL_2);
232                     animator.start();
233                 }
234                 mPassedAssistantSlop = false;
235                 break;
236         }
237         if (mTouchCameFromNavBar && mGestureCallback != null) {
238             mGestureCallback.setNavBarGestureProgress(event.getY() - mDownPos.y);
239         }
240         mSwipeUpTouchTracker.onMotionEvent(event);
241         mAssistantGestureDetector.onTouchEvent(event);
242         return intercepted;
243     }
244 
245     /**
246      * Determine if angle is larger than threshold for assistant detection
247      */
isValidAssistantGestureAngle(float deltaX, float deltaY)248     private boolean isValidAssistantGestureAngle(float deltaX, float deltaY) {
249         float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
250 
251         // normalize so that angle is measured clockwise from horizontal in the bottom right corner
252         // and counterclockwise from horizontal in the bottom left corner
253         angle = angle > 90 ? 180 - angle : angle;
254         return (angle > mAssistantAngleThreshold && angle < 90);
255     }
256 
updateAssistantProgress()257     private void updateAssistantProgress() {
258         if (!mLaunchedAssistant) {
259             mAssistantLastProgress =
260                     Math.min(mAssistantDistance * 1f / mAssistantDragDistThreshold, 1)
261                             * mAssistantTimeFraction;
262             if (mAssistantDistance >= mAssistantDragDistThreshold && mAssistantTimeFraction >= 1) {
263                 startAssistant(new PointF());
264             } else if (mGestureCallback != null) {
265                 mGestureCallback.setAssistantProgress(mAssistantLastProgress);
266             }
267         }
268     }
269 
startAssistant(PointF velocity)270     private void startAssistant(PointF velocity) {
271         if (mGestureCallback != null) {
272             mGestureCallback.onNavBarGestureAttempted(ASSISTANT_COMPLETED, velocity);
273         }
274         VibratorWrapper.INSTANCE.get(mContext).vibrate(VibratorWrapper.EFFECT_CLICK);
275         mLaunchedAssistant = true;
276     }
277 
278     enum NavBarGestureResult {
279         UNKNOWN,
280         HOME_GESTURE_COMPLETED,
281         OVERVIEW_GESTURE_COMPLETED,
282         HOME_NOT_STARTED_TOO_FAR_FROM_EDGE,
283         OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE,
284         HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION,  // Side swipe on nav bar.
285         HOME_OR_OVERVIEW_CANCELLED,
286         ASSISTANT_COMPLETED,
287         ASSISTANT_NOT_STARTED_BAD_ANGLE,
288         ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT,
289     }
290 
291     /** Callback to let the UI react to attempted nav bar gestures. */
292     interface NavBarGestureAttemptCallback {
293         /** Called whenever any touch is completed. */
onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity)294         void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity);
295 
296         /** Indicates how far a touch originating in the nav bar has moved from the nav bar. */
setNavBarGestureProgress(@ullable Float displacement)297         default void setNavBarGestureProgress(@Nullable Float displacement) {}
298 
299         /** Indicates the progress of an Assistant gesture. */
setAssistantProgress(float progress)300         default void setAssistantProgress(float progress) {}
301     }
302 
303     private class AssistantGestureListener extends GestureDetector.SimpleOnGestureListener {
304         @Override
onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)305         public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
306             if (!mLaunchedAssistant && mTouchCameFromAssistantCorner) {
307                 PointF velocity = new PointF(velocityX, velocityY);
308                 if (!isValidAssistantGestureAngle(velocityX, -velocityY)) {
309                     if (mGestureCallback != null) {
310                         mGestureCallback.onNavBarGestureAttempted(ASSISTANT_NOT_STARTED_BAD_ANGLE,
311                                 velocity);
312                     }
313                 } else if (mAssistantDistance >= mAssistantFlingDistThreshold) {
314                     mAssistantLastProgress = 1;
315                     startAssistant(velocity);
316                 }
317             }
318             return true;
319         }
320     }
321 }
322