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.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
19 
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.graphics.Point;
23 import android.graphics.PointF;
24 import android.os.SystemProperties;
25 import android.view.MotionEvent;
26 import android.view.View;
27 import android.view.View.OnTouchListener;
28 import android.view.ViewConfiguration;
29 import android.view.ViewGroup;
30 import android.view.ViewGroup.LayoutParams;
31 
32 import androidx.annotation.Nullable;
33 
34 import com.android.launcher3.Utilities;
35 import com.android.launcher3.testing.shared.ResourceUtils;
36 import com.android.launcher3.util.DisplayController;
37 
38 /**
39  * Utility class to handle edge swipes for back gestures.
40  *
41  * Forked from platform/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java.
42  */
43 public class EdgeBackGestureHandler implements OnTouchListener {
44 
45     private static final String TAG = "EdgeBackGestureHandler";
46     private static final int MAX_LONG_PRESS_TIMEOUT = SystemProperties.getInt(
47             "gestures.back_timeout", 250);
48 
49     private final Context mContext;
50 
51     private final Point mDisplaySize = new Point();
52 
53     // The edge width where touch down is allowed
54     private final int mEdgeWidth;
55     // The bottom gesture area height
56     private final int mBottomGestureHeight;
57     // The slop to distinguish between horizontal and vertical motion
58     private final float mTouchSlop;
59     // Duration after which we consider the event as longpress.
60     private final int mLongPressTimeout;
61 
62     private final PointF mDownPoint = new PointF();
63     private boolean mThresholdCrossed = false;
64     private boolean mAllowGesture = false;
65     private BackGestureResult mDisallowedGestureReason;
66     private boolean mIsEnabled;
67     private int mLeftInset;
68     private int mRightInset;
69 
70     private EdgeBackGesturePanel mEdgeBackPanel;
71     private BackGestureAttemptCallback mGestureCallback;
72 
73     private final EdgeBackGesturePanel.BackCallback mBackCallback =
74             new EdgeBackGesturePanel.BackCallback() {
75                 @Override
76                 public void triggerBack() {
77                     if (mGestureCallback != null) {
78                         mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel()
79                                 ? BackGestureResult.BACK_COMPLETED_FROM_LEFT
80                                 : BackGestureResult.BACK_COMPLETED_FROM_RIGHT);
81                     }
82                 }
83 
84                 @Override
85                 public void cancelBack() {
86                     if (mGestureCallback != null) {
87                         mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel()
88                                 ? BackGestureResult.BACK_CANCELLED_FROM_LEFT
89                                 : BackGestureResult.BACK_CANCELLED_FROM_RIGHT);
90                     }
91                 }
92             };
93 
EdgeBackGestureHandler(Context context)94     EdgeBackGestureHandler(Context context) {
95         final Resources res = context.getResources();
96         mContext = context;
97 
98         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
99         mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT,
100                 ViewConfiguration.getLongPressTimeout());
101 
102         mBottomGestureHeight =
103             ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, res);
104         int systemBackRegion = ResourceUtils.getNavbarSize("config_backGestureInset", res);
105         // System back region is 0 if gesture nav is not currently enabled.
106         mEdgeWidth = systemBackRegion == 0 ? Utilities.dpToPx(18) : systemBackRegion;
107     }
108 
setViewGroupParent(@ullable ViewGroup parent)109     void setViewGroupParent(@Nullable ViewGroup parent) {
110         mIsEnabled = parent != null;
111 
112         if (mEdgeBackPanel != null) {
113             mEdgeBackPanel.onDestroy();
114             mEdgeBackPanel = null;
115         }
116 
117         if (mIsEnabled) {
118             // Add a nav bar panel window.
119             mEdgeBackPanel = new EdgeBackGesturePanel(mContext, parent, createLayoutParams());
120             mEdgeBackPanel.setBackCallback(mBackCallback);
121             Point currentSize = DisplayController.INSTANCE.get(mContext).getInfo().currentSize;
122             mDisplaySize.set(currentSize.x, currentSize.y);
123             mEdgeBackPanel.setDisplaySize(mDisplaySize);
124         }
125     }
126 
registerBackGestureAttemptCallback(BackGestureAttemptCallback callback)127     void registerBackGestureAttemptCallback(BackGestureAttemptCallback callback) {
128         mGestureCallback = callback;
129     }
130 
unregisterBackGestureAttemptCallback()131     void unregisterBackGestureAttemptCallback() {
132         mGestureCallback = null;
133     }
134 
createLayoutParams()135     private LayoutParams createLayoutParams() {
136         Resources resources = mContext.getResources();
137         return new LayoutParams(
138                 ResourceUtils.getNavbarSize("navigation_edge_panel_width", resources),
139                 ResourceUtils.getNavbarSize("navigation_edge_panel_height", resources));
140     }
141 
142     @Override
onTouch(View view, MotionEvent motionEvent)143     public boolean onTouch(View view, MotionEvent motionEvent) {
144         if (mIsEnabled) {
145             onMotionEvent(motionEvent);
146             return true;
147         }
148         return false;
149     }
150 
onInterceptTouch(MotionEvent motionEvent)151     boolean onInterceptTouch(MotionEvent motionEvent) {
152         return isWithinTouchRegion((int) motionEvent.getX(), (int) motionEvent.getY());
153     }
154 
isWithinTouchRegion(int x, int y)155     private boolean isWithinTouchRegion(int x, int y) {
156         // Disallow if too far from the edge
157         if (x > mEdgeWidth + mLeftInset && x < (mDisplaySize.x - mEdgeWidth - mRightInset)) {
158             mDisallowedGestureReason = BackGestureResult.BACK_NOT_STARTED_TOO_FAR_FROM_EDGE;
159             return false;
160         }
161 
162         // Disallow if we are in the bottom gesture area
163         if (y >= (mDisplaySize.y - mBottomGestureHeight)) {
164             mDisallowedGestureReason = BackGestureResult.BACK_NOT_STARTED_IN_NAV_BAR_REGION;
165             return false;
166         }
167 
168         return true;
169     }
170 
cancelGesture(MotionEvent ev)171     private void cancelGesture(MotionEvent ev) {
172         // Send action cancel to reset all the touch events
173         mAllowGesture = false;
174         MotionEvent cancelEv = MotionEvent.obtain(ev);
175         cancelEv.setAction(MotionEvent.ACTION_CANCEL);
176         mEdgeBackPanel.onMotionEvent(cancelEv);
177         cancelEv.recycle();
178     }
179 
onMotionEvent(MotionEvent ev)180     private void onMotionEvent(MotionEvent ev) {
181         int action = ev.getActionMasked();
182         if (action == MotionEvent.ACTION_DOWN) {
183             boolean isOnLeftEdge = ev.getX() <= mEdgeWidth + mLeftInset;
184             mDisallowedGestureReason = BackGestureResult.UNKNOWN;
185             mAllowGesture = isWithinTouchRegion((int) ev.getX(), (int) ev.getY());
186             mDownPoint.set(ev.getX(), ev.getY());
187             if (mAllowGesture) {
188                 mEdgeBackPanel.setIsLeftPanel(isOnLeftEdge);
189                 mEdgeBackPanel.onMotionEvent(ev);
190                 mThresholdCrossed = false;
191             }
192         } else if (mAllowGesture) {
193             if (!mThresholdCrossed) {
194                 if (action == MotionEvent.ACTION_POINTER_DOWN) {
195                     // We do not support multi touch for back gesture
196                     cancelGesture(ev);
197                     return;
198                 } else if (action == MotionEvent.ACTION_MOVE) {
199                     if ((ev.getEventTime() - ev.getDownTime()) > mLongPressTimeout) {
200                         cancelGesture(ev);
201                         return;
202                     }
203                     float dx = Math.abs(ev.getX() - mDownPoint.x);
204                     float dy = Math.abs(ev.getY() - mDownPoint.y);
205                     if (dy > dx && dy > mTouchSlop) {
206                         cancelGesture(ev);
207                         return;
208                     } else if (dx > dy && dx > mTouchSlop) {
209                         mThresholdCrossed = true;
210                     }
211                 }
212             }
213 
214             if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
215                 mGestureCallback.onBackGestureProgress(ev.getX() - mDownPoint.x,
216                         ev.getY() - mDownPoint.y, mEdgeBackPanel.getIsLeftPanel());
217             }
218 
219             // forward touch
220             mEdgeBackPanel.onMotionEvent(ev);
221         }
222 
223         if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
224             float dx = Math.abs(ev.getX() - mDownPoint.x);
225             float dy = Math.abs(ev.getY() - mDownPoint.y);
226             if (dx > dy && dx > mTouchSlop && !mAllowGesture && mGestureCallback != null) {
227                 mGestureCallback.onBackGestureAttempted(mDisallowedGestureReason);
228             }
229         }
230     }
231 
setInsets(int leftInset, int rightInset)232     void setInsets(int leftInset, int rightInset) {
233         mLeftInset = leftInset;
234         mRightInset = rightInset;
235     }
236 
237     enum BackGestureResult {
238         UNKNOWN,
239         BACK_COMPLETED_FROM_LEFT,
240         BACK_COMPLETED_FROM_RIGHT,
241         BACK_CANCELLED_FROM_LEFT,
242         BACK_CANCELLED_FROM_RIGHT,
243         BACK_NOT_STARTED_TOO_FAR_FROM_EDGE,
244         BACK_NOT_STARTED_IN_NAV_BAR_REGION,
245     }
246 
247     /** Callback to let the UI react to attempted back gestures. */
248     interface BackGestureAttemptCallback {
249         /** Called whenever any touch is completed. */
onBackGestureAttempted(BackGestureResult result)250         void onBackGestureAttempted(BackGestureResult result);
251 
252         /** Called when the back gesture is recognized and is in progress. */
onBackGestureProgress(float diffx, float diffy, boolean isLeftGesture)253         default void onBackGestureProgress(float diffx, float diffy, boolean isLeftGesture) {}
254     }
255 }
256