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