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