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