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.util.VibratorWrapper.OVERVIEW_HAPTIC;
19 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_GESTURE_COMPLETED;
20 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_NOT_STARTED_TOO_FAR_FROM_EDGE;
21 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_CANCELLED;
22 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION;
23 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_GESTURE_COMPLETED;
24 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE;
25 
26 import android.content.Context;
27 import android.content.res.Resources;
28 import android.graphics.Point;
29 import android.graphics.PointF;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.view.View.OnTouchListener;
33 
34 import androidx.annotation.Nullable;
35 
36 import com.android.launcher3.testing.shared.ResourceUtils;
37 import com.android.launcher3.util.DisplayController;
38 import com.android.launcher3.util.NavigationMode;
39 import com.android.launcher3.util.VibratorWrapper;
40 import com.android.quickstep.util.MotionPauseDetector;
41 import com.android.quickstep.util.NavBarPosition;
42 import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
43 
44 /** Utility class to handle Home gesture. */
45 public class NavBarGestureHandler implements OnTouchListener,
46         TriggerSwipeUpTouchTracker.OnSwipeUpListener, MotionPauseDetector.OnMotionPauseListener {
47 
48     private static final String LOG_TAG = "NavBarGestureHandler";
49     private final Context mContext;
50     private final Point mDisplaySize = new Point();
51     private final TriggerSwipeUpTouchTracker mSwipeUpTouchTracker;
52     private final int mBottomGestureHeight;
53     private final PointF mDownPos = new PointF();
54     private final PointF mLastPos = new PointF();
55     private final MotionPauseDetector mMotionPauseDetector;
56     private boolean mTouchCameFromNavBar;
57     @Nullable
58     private NavBarGestureAttemptCallback mGestureCallback;
59 
NavBarGestureHandler(Context context)60     NavBarGestureHandler(Context context) {
61         mContext = context;
62         DisplayController.Info displayInfo = DisplayController.INSTANCE.get(mContext).getInfo();
63         Point currentSize = displayInfo.currentSize;
64         mDisplaySize.set(currentSize.x, currentSize.y);
65         mSwipeUpTouchTracker =
66                 new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/,
67                         new NavBarPosition(NavigationMode.NO_BUTTON, displayInfo),
68                         this);
69         mMotionPauseDetector = new MotionPauseDetector(context);
70 
71         final Resources resources = context.getResources();
72         mBottomGestureHeight =
73                 ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, resources);
74     }
75 
registerNavBarGestureAttemptCallback(NavBarGestureAttemptCallback callback)76     void registerNavBarGestureAttemptCallback(NavBarGestureAttemptCallback callback) {
77         mGestureCallback = callback;
78     }
79 
unregisterNavBarGestureAttemptCallback()80     void unregisterNavBarGestureAttemptCallback() {
81         mGestureCallback = null;
82     }
83 
84     @Override
onSwipeUp(boolean wasFling, PointF finalVelocity)85     public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
86         if (mGestureCallback == null) {
87             return;
88         }
89         if (mTouchCameFromNavBar) {
90             mGestureCallback.onNavBarGestureAttempted(wasFling
91                     ? HOME_GESTURE_COMPLETED : OVERVIEW_GESTURE_COMPLETED, finalVelocity);
92         } else {
93             mGestureCallback.onNavBarGestureAttempted(wasFling
94                     ? HOME_NOT_STARTED_TOO_FAR_FROM_EDGE : OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE,
95                     finalVelocity);
96         }
97     }
98 
99     @Override
onSwipeUpCancelled()100     public void onSwipeUpCancelled() {
101         if (mGestureCallback != null) {
102             mGestureCallback.onNavBarGestureAttempted(HOME_OR_OVERVIEW_CANCELLED, new PointF());
103         }
104     }
105 
106     @Override
onTouch(View view, MotionEvent event)107     public boolean onTouch(View view, MotionEvent event) {
108         int action = event.getAction();
109         boolean intercepted = mSwipeUpTouchTracker.interceptedTouch();
110         switch (action) {
111             case MotionEvent.ACTION_DOWN:
112                 mDownPos.set(event.getX(), event.getY());
113                 mLastPos.set(mDownPos);
114                 mTouchCameFromNavBar = mDownPos.y >= mDisplaySize.y - mBottomGestureHeight;
115                 if (!mTouchCameFromNavBar && mGestureCallback != null) {
116                     mGestureCallback.setNavBarGestureProgress(null);
117                 }
118                 mSwipeUpTouchTracker.init();
119                 mMotionPauseDetector.clear();
120                 mMotionPauseDetector.setOnMotionPauseListener(this);
121                 break;
122             case MotionEvent.ACTION_MOVE:
123                 mLastPos.set(event.getX(), event.getY());
124                 break;
125             case MotionEvent.ACTION_UP:
126             case MotionEvent.ACTION_CANCEL:
127                 mMotionPauseDetector.clear();
128                 if (mGestureCallback != null && !intercepted && mTouchCameFromNavBar) {
129                     mGestureCallback.onNavBarGestureAttempted(
130                             HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION, new PointF());
131                     intercepted = true;
132                     break;
133                 }
134                 break;
135         }
136         if (mTouchCameFromNavBar && mGestureCallback != null) {
137             mGestureCallback.setNavBarGestureProgress(event.getY() - mDownPos.y);
138         }
139         mSwipeUpTouchTracker.onMotionEvent(event);
140         mMotionPauseDetector.addPosition(event);
141         mMotionPauseDetector.setDisallowPause(mLastPos.y >= mDisplaySize.y - mBottomGestureHeight);
142         return intercepted;
143     }
144 
onInterceptTouch(MotionEvent event)145     boolean onInterceptTouch(MotionEvent event) {
146         return event.getY() >= mDisplaySize.y - mBottomGestureHeight;
147     }
148 
149     @Override
onMotionPauseChanged(boolean isPaused)150     public void onMotionPauseChanged(boolean isPaused) {
151         mGestureCallback.onMotionPaused(isPaused);
152     }
153 
154     @Override
onMotionPauseDetected()155     public void onMotionPauseDetected() {
156         VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
157     }
158 
159     enum NavBarGestureResult {
160         UNKNOWN,
161         HOME_GESTURE_COMPLETED,
162         OVERVIEW_GESTURE_COMPLETED,
163         HOME_NOT_STARTED_TOO_FAR_FROM_EDGE,
164         OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE,
165         HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION,  // Side swipe on nav bar.
166         HOME_OR_OVERVIEW_CANCELLED,
167     }
168 
169     /** Callback to let the UI react to attempted nav bar gestures. */
170     interface NavBarGestureAttemptCallback {
171         /** Called whenever any touch is completed. */
onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity)172         void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity);
173 
174         /** Called when a motion stops or resumes */
onMotionPaused(boolean isPaused)175         default void onMotionPaused(boolean isPaused) {}
176 
177         /** Indicates how far a touch originating in the nav bar has moved from the nav bar. */
setNavBarGestureProgress(@ullable Float displacement)178         default void setNavBarGestureProgress(@Nullable Float displacement) {}
179     }
180 }
181