1 /** 2 * Copyright (C) 2019 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.systemui.statusbar.phone; 17 18 import static android.view.Display.INVALID_DISPLAY; 19 20 import android.content.Context; 21 import android.content.pm.ParceledListSlice; 22 import android.content.res.Resources; 23 import android.graphics.PixelFormat; 24 import android.graphics.Point; 25 import android.graphics.PointF; 26 import android.graphics.Rect; 27 import android.graphics.Region; 28 import android.hardware.display.DisplayManager; 29 import android.hardware.display.DisplayManager.DisplayListener; 30 import android.hardware.input.InputManager; 31 import android.os.Looper; 32 import android.os.RemoteException; 33 import android.os.SystemClock; 34 import android.util.Log; 35 import android.util.MathUtils; 36 import android.view.Gravity; 37 import android.view.IPinnedStackController; 38 import android.view.IPinnedStackListener; 39 import android.view.ISystemGestureExclusionListener; 40 import android.view.InputChannel; 41 import android.view.InputDevice; 42 import android.view.InputEvent; 43 import android.view.InputEventReceiver; 44 import android.view.InputMonitor; 45 import android.view.KeyCharacterMap; 46 import android.view.KeyEvent; 47 import android.view.MotionEvent; 48 import android.view.View; 49 import android.view.ViewConfiguration; 50 import android.view.WindowManager; 51 import android.view.WindowManagerGlobal; 52 53 import com.android.systemui.Dependency; 54 import com.android.systemui.R; 55 import com.android.systemui.bubbles.BubbleController; 56 import com.android.systemui.recents.OverviewProxyService; 57 import com.android.systemui.shared.system.QuickStepContract; 58 import com.android.systemui.shared.system.WindowManagerWrapper; 59 60 import java.io.PrintWriter; 61 import java.util.concurrent.Executor; 62 63 /** 64 * Utility class to handle edge swipes for back gesture 65 */ 66 public class EdgeBackGestureHandler implements DisplayListener { 67 68 private static final String TAG = "EdgeBackGestureHandler"; 69 private static final int MAX_LONG_PRESS_TIMEOUT = 250; 70 71 private final IPinnedStackListener.Stub mImeChangedListener = new IPinnedStackListener.Stub() { 72 @Override 73 public void onListenerRegistered(IPinnedStackController controller) { 74 } 75 76 @Override 77 public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { 78 // No need to thread jump, assignments are atomic 79 mImeHeight = imeVisible ? imeHeight : 0; 80 // TODO: Probably cancel any existing gesture 81 } 82 83 @Override 84 public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) { 85 } 86 87 @Override 88 public void onMinimizedStateChanged(boolean isMinimized) { 89 } 90 91 @Override 92 public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, 93 Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment, 94 int displayRotation) { 95 } 96 97 @Override 98 public void onActionsChanged(ParceledListSlice actions) { 99 } 100 }; 101 102 private ISystemGestureExclusionListener mGestureExclusionListener = 103 new ISystemGestureExclusionListener.Stub() { 104 @Override 105 public void onSystemGestureExclusionChanged(int displayId, 106 Region systemGestureExclusion) { 107 if (displayId == mDisplayId) { 108 mMainExecutor.execute(() -> mExcludeRegion.set(systemGestureExclusion)); 109 } 110 } 111 }; 112 113 private final Context mContext; 114 private final OverviewProxyService mOverviewProxyService; 115 116 private final Point mDisplaySize = new Point(); 117 private final int mDisplayId; 118 119 private final Executor mMainExecutor; 120 121 private final Region mExcludeRegion = new Region(); 122 // The edge width where touch down is allowed 123 private int mEdgeWidth; 124 // The slop to distinguish between horizontal and vertical motion 125 private final float mTouchSlop; 126 // Duration after which we consider the event as longpress. 127 private final int mLongPressTimeout; 128 // The threshold where the touch needs to be at most, such that the arrow is displayed above the 129 // finger, otherwise it will be below 130 private final int mMinArrowPosition; 131 // The amount by which the arrow is shifted to avoid the finger 132 private final int mFingerOffset; 133 134 135 private final int mNavBarHeight; 136 137 private final PointF mDownPoint = new PointF(); 138 private boolean mThresholdCrossed = false; 139 private boolean mAllowGesture = false; 140 private boolean mIsOnLeftEdge; 141 142 private int mImeHeight = 0; 143 144 private boolean mIsAttached; 145 private boolean mIsGesturalModeEnabled; 146 private boolean mIsEnabled; 147 148 private InputMonitor mInputMonitor; 149 private InputEventReceiver mInputEventReceiver; 150 151 private final WindowManager mWm; 152 153 private NavigationBarEdgePanel mEdgePanel; 154 private WindowManager.LayoutParams mEdgePanelLp; 155 private final Rect mSamplingRect = new Rect(); 156 private RegionSamplingHelper mRegionSamplingHelper; 157 private int mLeftInset; 158 private int mRightInset; 159 EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService)160 public EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService) { 161 final Resources res = context.getResources(); 162 mContext = context; 163 mDisplayId = context.getDisplayId(); 164 mMainExecutor = context.getMainExecutor(); 165 mWm = context.getSystemService(WindowManager.class); 166 mOverviewProxyService = overviewProxyService; 167 168 // Reduce the default touch slop to ensure that we can intercept the gesture 169 // before the app starts to react to it. 170 // TODO(b/130352502) Tune this value and extract into a constant 171 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 0.75f; 172 mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT, 173 ViewConfiguration.getLongPressTimeout()); 174 175 mNavBarHeight = res.getDimensionPixelSize(R.dimen.navigation_bar_frame_height); 176 mMinArrowPosition = res.getDimensionPixelSize(R.dimen.navigation_edge_arrow_min_y); 177 mFingerOffset = res.getDimensionPixelSize(R.dimen.navigation_edge_finger_offset); 178 updateCurrentUserResources(res); 179 } 180 updateCurrentUserResources(Resources res)181 public void updateCurrentUserResources(Resources res) { 182 mEdgeWidth = res.getDimensionPixelSize( 183 com.android.internal.R.dimen.config_backGestureInset); 184 } 185 186 /** 187 * @see NavigationBarView#onAttachedToWindow() 188 */ onNavBarAttached()189 public void onNavBarAttached() { 190 mIsAttached = true; 191 updateIsEnabled(); 192 } 193 194 /** 195 * @see NavigationBarView#onDetachedFromWindow() 196 */ onNavBarDetached()197 public void onNavBarDetached() { 198 mIsAttached = false; 199 updateIsEnabled(); 200 } 201 onNavigationModeChanged(int mode, Context currentUserContext)202 public void onNavigationModeChanged(int mode, Context currentUserContext) { 203 mIsGesturalModeEnabled = QuickStepContract.isGesturalMode(mode); 204 updateIsEnabled(); 205 updateCurrentUserResources(currentUserContext.getResources()); 206 } 207 disposeInputChannel()208 private void disposeInputChannel() { 209 if (mInputEventReceiver != null) { 210 mInputEventReceiver.dispose(); 211 mInputEventReceiver = null; 212 } 213 if (mInputMonitor != null) { 214 mInputMonitor.dispose(); 215 mInputMonitor = null; 216 } 217 } 218 updateIsEnabled()219 private void updateIsEnabled() { 220 boolean isEnabled = mIsAttached && mIsGesturalModeEnabled; 221 if (isEnabled == mIsEnabled) { 222 return; 223 } 224 mIsEnabled = isEnabled; 225 disposeInputChannel(); 226 227 if (mEdgePanel != null) { 228 mWm.removeView(mEdgePanel); 229 mEdgePanel = null; 230 mRegionSamplingHelper.stop(); 231 mRegionSamplingHelper = null; 232 } 233 234 if (!mIsEnabled) { 235 WindowManagerWrapper.getInstance().removePinnedStackListener(mImeChangedListener); 236 mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this); 237 238 try { 239 WindowManagerGlobal.getWindowManagerService() 240 .unregisterSystemGestureExclusionListener( 241 mGestureExclusionListener, mDisplayId); 242 } catch (RemoteException e) { 243 Log.e(TAG, "Failed to unregister window manager callbacks", e); 244 } 245 246 } else { 247 updateDisplaySize(); 248 mContext.getSystemService(DisplayManager.class).registerDisplayListener(this, 249 mContext.getMainThreadHandler()); 250 251 try { 252 WindowManagerWrapper.getInstance().addPinnedStackListener(mImeChangedListener); 253 WindowManagerGlobal.getWindowManagerService() 254 .registerSystemGestureExclusionListener( 255 mGestureExclusionListener, mDisplayId); 256 } catch (RemoteException e) { 257 Log.e(TAG, "Failed to register window manager callbacks", e); 258 } 259 260 // Register input event receiver 261 mInputMonitor = InputManager.getInstance().monitorGestureInput( 262 "edge-swipe", mDisplayId); 263 mInputEventReceiver = new SysUiInputEventReceiver( 264 mInputMonitor.getInputChannel(), Looper.getMainLooper()); 265 266 // Add a nav bar panel window 267 mEdgePanel = new NavigationBarEdgePanel(mContext); 268 mEdgePanelLp = new WindowManager.LayoutParams( 269 mContext.getResources() 270 .getDimensionPixelSize(R.dimen.navigation_edge_panel_width), 271 mContext.getResources() 272 .getDimensionPixelSize(R.dimen.navigation_edge_panel_height), 273 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, 274 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 275 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 276 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH 277 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, 278 PixelFormat.TRANSLUCENT); 279 mEdgePanelLp.privateFlags |= 280 WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; 281 mEdgePanelLp.setTitle(TAG + mDisplayId); 282 mEdgePanelLp.accessibilityTitle = mContext.getString(R.string.nav_bar_edge_panel); 283 mEdgePanelLp.windowAnimations = 0; 284 mEdgePanel.setLayoutParams(mEdgePanelLp); 285 mWm.addView(mEdgePanel, mEdgePanelLp); 286 mRegionSamplingHelper = new RegionSamplingHelper(mEdgePanel, 287 new RegionSamplingHelper.SamplingCallback() { 288 @Override 289 public void onRegionDarknessChanged(boolean isRegionDark) { 290 mEdgePanel.setIsDark(!isRegionDark, true /* animate */); 291 } 292 293 @Override 294 public Rect getSampledRegion(View sampledView) { 295 return mSamplingRect; 296 } 297 }); 298 } 299 } 300 onInputEvent(InputEvent ev)301 private void onInputEvent(InputEvent ev) { 302 if (ev instanceof MotionEvent) { 303 onMotionEvent((MotionEvent) ev); 304 } 305 } 306 isWithinTouchRegion(int x, int y)307 private boolean isWithinTouchRegion(int x, int y) { 308 if (y > (mDisplaySize.y - Math.max(mImeHeight, mNavBarHeight))) { 309 return false; 310 } 311 312 if (x > mEdgeWidth + mLeftInset && x < (mDisplaySize.x - mEdgeWidth - mRightInset)) { 313 return false; 314 } 315 boolean isInExcludedRegion = mExcludeRegion.contains(x, y); 316 if (isInExcludedRegion) { 317 mOverviewProxyService.notifyBackAction(false /* completed */, -1, -1, 318 false /* isButton */, !mIsOnLeftEdge); 319 } 320 return !isInExcludedRegion; 321 } 322 cancelGesture(MotionEvent ev)323 private void cancelGesture(MotionEvent ev) { 324 // Send action cancel to reset all the touch events 325 mAllowGesture = false; 326 MotionEvent cancelEv = MotionEvent.obtain(ev); 327 cancelEv.setAction(MotionEvent.ACTION_CANCEL); 328 mEdgePanel.handleTouch(cancelEv); 329 cancelEv.recycle(); 330 } 331 onMotionEvent(MotionEvent ev)332 private void onMotionEvent(MotionEvent ev) { 333 int action = ev.getActionMasked(); 334 if (action == MotionEvent.ACTION_DOWN) { 335 // Verify if this is in within the touch region and we aren't in immersive mode, and 336 // either the bouncer is showing or the notification panel is hidden 337 int stateFlags = mOverviewProxyService.getSystemUiStateFlags(); 338 mIsOnLeftEdge = ev.getX() <= mEdgeWidth + mLeftInset; 339 mAllowGesture = !QuickStepContract.isBackGestureDisabled(stateFlags) 340 && isWithinTouchRegion((int) ev.getX(), (int) ev.getY()); 341 if (mAllowGesture) { 342 mEdgePanelLp.gravity = mIsOnLeftEdge 343 ? (Gravity.LEFT | Gravity.TOP) 344 : (Gravity.RIGHT | Gravity.TOP); 345 mEdgePanel.setIsLeftPanel(mIsOnLeftEdge); 346 mEdgePanel.handleTouch(ev); 347 updateEdgePanelPosition(ev.getY()); 348 mWm.updateViewLayout(mEdgePanel, mEdgePanelLp); 349 mRegionSamplingHelper.start(mSamplingRect); 350 351 mDownPoint.set(ev.getX(), ev.getY()); 352 mThresholdCrossed = false; 353 } 354 } else if (mAllowGesture) { 355 if (!mThresholdCrossed) { 356 if (action == MotionEvent.ACTION_POINTER_DOWN) { 357 // We do not support multi touch for back gesture 358 cancelGesture(ev); 359 return; 360 } else if (action == MotionEvent.ACTION_MOVE) { 361 if ((ev.getEventTime() - ev.getDownTime()) > mLongPressTimeout) { 362 cancelGesture(ev); 363 return; 364 } 365 float dx = Math.abs(ev.getX() - mDownPoint.x); 366 float dy = Math.abs(ev.getY() - mDownPoint.y); 367 if (dy > dx && dy > mTouchSlop) { 368 cancelGesture(ev); 369 return; 370 371 } else if (dx > dy && dx > mTouchSlop) { 372 mThresholdCrossed = true; 373 // Capture inputs 374 mInputMonitor.pilferPointers(); 375 } 376 } 377 378 } 379 380 // forward touch 381 mEdgePanel.handleTouch(ev); 382 383 boolean isUp = action == MotionEvent.ACTION_UP; 384 if (isUp) { 385 boolean performAction = mEdgePanel.shouldTriggerBack(); 386 if (performAction) { 387 // Perform back 388 sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK); 389 sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK); 390 } 391 mOverviewProxyService.notifyBackAction(performAction, (int) mDownPoint.x, 392 (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge); 393 } 394 if (isUp || action == MotionEvent.ACTION_CANCEL) { 395 mRegionSamplingHelper.stop(); 396 } else { 397 updateSamplingRect(); 398 mRegionSamplingHelper.updateSamplingRect(); 399 } 400 } 401 } 402 updateEdgePanelPosition(float touchY)403 private void updateEdgePanelPosition(float touchY) { 404 float position = touchY - mFingerOffset; 405 position = Math.max(position, mMinArrowPosition); 406 position = (position - mEdgePanelLp.height / 2.0f); 407 mEdgePanelLp.y = MathUtils.constrain((int) position, 0, mDisplaySize.y); 408 updateSamplingRect(); 409 } 410 updateSamplingRect()411 private void updateSamplingRect() { 412 int top = mEdgePanelLp.y; 413 int left = mIsOnLeftEdge ? mLeftInset : mDisplaySize.x - mRightInset - mEdgePanelLp.width; 414 int right = left + mEdgePanelLp.width; 415 int bottom = top + mEdgePanelLp.height; 416 mSamplingRect.set(left, top, right, bottom); 417 mEdgePanel.adjustRectToBoundingBox(mSamplingRect); 418 } 419 420 @Override onDisplayAdded(int displayId)421 public void onDisplayAdded(int displayId) { } 422 423 @Override onDisplayRemoved(int displayId)424 public void onDisplayRemoved(int displayId) { } 425 426 @Override onDisplayChanged(int displayId)427 public void onDisplayChanged(int displayId) { 428 if (displayId == mDisplayId) { 429 updateDisplaySize(); 430 } 431 } 432 updateDisplaySize()433 private void updateDisplaySize() { 434 mContext.getSystemService(DisplayManager.class) 435 .getDisplay(mDisplayId) 436 .getRealSize(mDisplaySize); 437 } 438 sendEvent(int action, int code)439 private void sendEvent(int action, int code) { 440 long when = SystemClock.uptimeMillis(); 441 final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */, 442 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 443 KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, 444 InputDevice.SOURCE_KEYBOARD); 445 446 // Bubble controller will give us a valid display id if it should get the back event 447 BubbleController bubbleController = Dependency.get(BubbleController.class); 448 int bubbleDisplayId = bubbleController.getExpandedDisplayId(mContext); 449 if (code == KeyEvent.KEYCODE_BACK && bubbleDisplayId != INVALID_DISPLAY) { 450 ev.setDisplayId(bubbleDisplayId); 451 } 452 InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); 453 } 454 setInsets(int leftInset, int rightInset)455 public void setInsets(int leftInset, int rightInset) { 456 mLeftInset = leftInset; 457 mRightInset = rightInset; 458 } 459 dump(PrintWriter pw)460 public void dump(PrintWriter pw) { 461 pw.println("EdgeBackGestureHandler:"); 462 pw.println(" mIsEnabled=" + mIsEnabled); 463 pw.println(" mAllowGesture=" + mAllowGesture); 464 pw.println(" mExcludeRegion=" + mExcludeRegion); 465 pw.println(" mImeHeight=" + mImeHeight); 466 pw.println(" mIsAttached=" + mIsAttached); 467 pw.println(" mEdgeWidth=" + mEdgeWidth); 468 } 469 470 class SysUiInputEventReceiver extends InputEventReceiver { SysUiInputEventReceiver(InputChannel channel, Looper looper)471 SysUiInputEventReceiver(InputChannel channel, Looper looper) { 472 super(channel, looper); 473 } 474 onInputEvent(InputEvent event)475 public void onInputEvent(InputEvent event) { 476 EdgeBackGestureHandler.this.onInputEvent(event); 477 finishInputEvent(event, true); 478 } 479 } 480 } 481