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