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