1 /* 2 * Copyright (C) 2018 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 17 package com.android.server.wm; 18 19 import static android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM; 20 import static android.view.DisplayCutout.BOUNDS_POSITION_LEFT; 21 import static android.view.DisplayCutout.BOUNDS_POSITION_RIGHT; 22 import static android.view.DisplayCutout.BOUNDS_POSITION_TOP; 23 import static android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT; 24 import static android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE; 25 26 import android.annotation.NonNull; 27 import android.content.Context; 28 import android.content.res.Resources; 29 import android.graphics.Rect; 30 import android.graphics.Region; 31 import android.hardware.display.DisplayManagerGlobal; 32 import android.os.Handler; 33 import android.os.SystemClock; 34 import android.util.Slog; 35 import android.view.Display; 36 import android.view.DisplayCutout; 37 import android.view.DisplayInfo; 38 import android.view.GestureDetector; 39 import android.view.InputDevice; 40 import android.view.MotionEvent; 41 import android.view.WindowManagerPolicyConstants.PointerEventListener; 42 import android.widget.OverScroller; 43 44 import java.io.PrintWriter; 45 46 /** 47 * Listens for system-wide input gestures, firing callbacks when detected. 48 * @hide 49 */ 50 class SystemGesturesPointerEventListener implements PointerEventListener { 51 private static final String TAG = "SystemGestures"; 52 private static final boolean DEBUG = false; 53 private static final long SWIPE_TIMEOUT_MS = 500; 54 private static final int MAX_TRACKED_POINTERS = 32; // max per input system 55 private static final int UNTRACKED_POINTER = -1; 56 private static final int MAX_FLING_TIME_MILLIS = 5000; 57 58 private static final int SWIPE_NONE = 0; 59 private static final int SWIPE_FROM_TOP = 1; 60 private static final int SWIPE_FROM_BOTTOM = 2; 61 private static final int SWIPE_FROM_RIGHT = 3; 62 private static final int SWIPE_FROM_LEFT = 4; 63 64 private static final int TRACKPAD_SWIPE_NONE = 0; 65 private static final int TRACKPAD_SWIPE_FROM_TOP = 1; 66 private static final int TRACKPAD_SWIPE_FROM_BOTTOM = 2; 67 private static final int TRACKPAD_SWIPE_FROM_RIGHT = 3; 68 private static final int TRACKPAD_SWIPE_FROM_LEFT = 4; 69 70 private final Context mContext; 71 private final Handler mHandler; 72 private int mDisplayCutoutTouchableRegionSize; 73 // The thresholds for each edge of the display 74 private final Rect mSwipeStartThreshold = new Rect(); 75 private int mSwipeDistanceThreshold; 76 private final Callbacks mCallbacks; 77 private final int[] mDownPointerId = new int[MAX_TRACKED_POINTERS]; 78 private final float[] mDownX = new float[MAX_TRACKED_POINTERS]; 79 private final float[] mDownY = new float[MAX_TRACKED_POINTERS]; 80 private final long[] mDownTime = new long[MAX_TRACKED_POINTERS]; 81 82 private GestureDetector mGestureDetector; 83 84 int screenHeight; 85 int screenWidth; 86 private int mDownPointers; 87 private boolean mSwipeFireable; 88 private boolean mDebugFireable; 89 private boolean mMouseHoveringAtLeft; 90 private boolean mMouseHoveringAtTop; 91 private boolean mMouseHoveringAtRight; 92 private boolean mMouseHoveringAtBottom; 93 private long mLastFlingTime; 94 SystemGesturesPointerEventListener(Context context, Handler handler, Callbacks callbacks)95 SystemGesturesPointerEventListener(Context context, Handler handler, Callbacks callbacks) { 96 mContext = checkNull("context", context); 97 mHandler = handler; 98 mCallbacks = checkNull("callbacks", callbacks); 99 onConfigurationChanged(); 100 } 101 onDisplayInfoChanged(DisplayInfo info)102 void onDisplayInfoChanged(DisplayInfo info) { 103 screenWidth = info.logicalWidth; 104 screenHeight = info.logicalHeight; 105 onConfigurationChanged(); 106 } 107 onConfigurationChanged()108 void onConfigurationChanged() { 109 final Resources r = mContext.getResources(); 110 final int defaultThreshold = r.getDimensionPixelSize( 111 com.android.internal.R.dimen.system_gestures_start_threshold); 112 mSwipeStartThreshold.set(defaultThreshold, defaultThreshold, defaultThreshold, 113 defaultThreshold); 114 mSwipeDistanceThreshold = defaultThreshold; 115 116 final Display display = DisplayManagerGlobal.getInstance() 117 .getRealDisplay(Display.DEFAULT_DISPLAY); 118 final DisplayCutout displayCutout = display.getCutout(); 119 if (displayCutout != null) { 120 // Expand swipe start threshold such that we can catch touches that just start beyond 121 // the notch area 122 mDisplayCutoutTouchableRegionSize = r.getDimensionPixelSize( 123 com.android.internal.R.dimen.display_cutout_touchable_region_size); 124 final Rect[] bounds = displayCutout.getBoundingRectsAll(); 125 if (bounds[BOUNDS_POSITION_LEFT] != null) { 126 mSwipeStartThreshold.left = Math.max(mSwipeStartThreshold.left, 127 bounds[BOUNDS_POSITION_LEFT].width() + mDisplayCutoutTouchableRegionSize); 128 } 129 if (bounds[BOUNDS_POSITION_TOP] != null) { 130 mSwipeStartThreshold.top = Math.max(mSwipeStartThreshold.top, 131 bounds[BOUNDS_POSITION_TOP].height() + mDisplayCutoutTouchableRegionSize); 132 } 133 if (bounds[BOUNDS_POSITION_RIGHT] != null) { 134 mSwipeStartThreshold.right = Math.max(mSwipeStartThreshold.right, 135 bounds[BOUNDS_POSITION_RIGHT].width() + mDisplayCutoutTouchableRegionSize); 136 } 137 if (bounds[BOUNDS_POSITION_BOTTOM] != null) { 138 mSwipeStartThreshold.bottom = Math.max(mSwipeStartThreshold.bottom, 139 bounds[BOUNDS_POSITION_BOTTOM].height() 140 + mDisplayCutoutTouchableRegionSize); 141 } 142 } 143 if (DEBUG) Slog.d(TAG, "mSwipeStartThreshold=" + mSwipeStartThreshold 144 + " mSwipeDistanceThreshold=" + mSwipeDistanceThreshold); 145 } 146 checkNull(String name, T arg)147 private static <T> T checkNull(String name, T arg) { 148 if (arg == null) { 149 throw new IllegalArgumentException(name + " must not be null"); 150 } 151 return arg; 152 } 153 systemReady()154 public void systemReady() { 155 // GestureDetector records statistics about gesture classification events to inform gesture 156 // usage trends. SystemGesturesPointerEventListener creates a lot of noise in these 157 // statistics because it passes every touch event though a GestureDetector. By creating an 158 // anonymous subclass of GestureDetector, these statistics will be recorded with a unique 159 // source name that can be filtered. 160 161 // GestureDetector would get a ViewConfiguration instance by context, that may also 162 // create a new WindowManagerImpl for the new display, and lock WindowManagerGlobal 163 // temporarily in the constructor that would make a deadlock. 164 mHandler.post(() -> { 165 final int displayId = mContext.getDisplayId(); 166 final DisplayInfo info = DisplayManagerGlobal.getInstance().getDisplayInfo(displayId); 167 if (info == null) { 168 // Display already removed, stop here. 169 Slog.w(TAG, "Cannot create GestureDetector, display removed:" + displayId); 170 return; 171 } 172 mGestureDetector = new GestureDetector(mContext, new FlingGestureDetector(), mHandler) { 173 }; 174 }); 175 } 176 177 @Override onPointerEvent(MotionEvent event)178 public void onPointerEvent(MotionEvent event) { 179 if (mGestureDetector != null && event.isTouchEvent()) { 180 mGestureDetector.onTouchEvent(event); 181 } 182 switch (event.getActionMasked()) { 183 case MotionEvent.ACTION_DOWN: 184 mSwipeFireable = true; 185 mDebugFireable = true; 186 mDownPointers = 0; 187 captureDown(event, 0); 188 if (mMouseHoveringAtLeft) { 189 mMouseHoveringAtLeft = false; 190 mCallbacks.onMouseLeaveFromLeft(); 191 } 192 if (mMouseHoveringAtTop) { 193 mMouseHoveringAtTop = false; 194 mCallbacks.onMouseLeaveFromTop(); 195 } 196 if (mMouseHoveringAtRight) { 197 mMouseHoveringAtRight = false; 198 mCallbacks.onMouseLeaveFromRight(); 199 } 200 if (mMouseHoveringAtBottom) { 201 mMouseHoveringAtBottom = false; 202 mCallbacks.onMouseLeaveFromBottom(); 203 } 204 mCallbacks.onDown(); 205 break; 206 case MotionEvent.ACTION_POINTER_DOWN: 207 captureDown(event, event.getActionIndex()); 208 if (mDebugFireable) { 209 mDebugFireable = event.getPointerCount() < 5; 210 if (!mDebugFireable) { 211 if (DEBUG) Slog.d(TAG, "Firing debug"); 212 mCallbacks.onDebug(); 213 } 214 } 215 break; 216 case MotionEvent.ACTION_MOVE: 217 if (mSwipeFireable) { 218 int trackpadSwipe = detectTrackpadThreeFingerSwipe(event); 219 mSwipeFireable = trackpadSwipe == TRACKPAD_SWIPE_NONE; 220 if (!mSwipeFireable) { 221 if (trackpadSwipe == TRACKPAD_SWIPE_FROM_TOP) { 222 if (DEBUG) Slog.d(TAG, "Firing onSwipeFromTop from trackpad"); 223 mCallbacks.onSwipeFromTop(); 224 } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_BOTTOM) { 225 if (DEBUG) Slog.d(TAG, "Firing onSwipeFromBottom from trackpad"); 226 mCallbacks.onSwipeFromBottom(); 227 } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_RIGHT) { 228 if (DEBUG) Slog.d(TAG, "Firing onSwipeFromRight from trackpad"); 229 mCallbacks.onSwipeFromRight(); 230 } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_LEFT) { 231 if (DEBUG) Slog.d(TAG, "Firing onSwipeFromLeft from trackpad"); 232 mCallbacks.onSwipeFromLeft(); 233 } 234 break; 235 } 236 237 final int swipe = detectSwipe(event); 238 mSwipeFireable = swipe == SWIPE_NONE; 239 if (swipe == SWIPE_FROM_TOP) { 240 if (DEBUG) Slog.d(TAG, "Firing onSwipeFromTop"); 241 mCallbacks.onSwipeFromTop(); 242 } else if (swipe == SWIPE_FROM_BOTTOM) { 243 if (DEBUG) Slog.d(TAG, "Firing onSwipeFromBottom"); 244 mCallbacks.onSwipeFromBottom(); 245 } else if (swipe == SWIPE_FROM_RIGHT) { 246 if (DEBUG) Slog.d(TAG, "Firing onSwipeFromRight"); 247 mCallbacks.onSwipeFromRight(); 248 } else if (swipe == SWIPE_FROM_LEFT) { 249 if (DEBUG) Slog.d(TAG, "Firing onSwipeFromLeft"); 250 mCallbacks.onSwipeFromLeft(); 251 } 252 } 253 break; 254 case MotionEvent.ACTION_HOVER_MOVE: 255 if (event.isFromSource(InputDevice.SOURCE_MOUSE)) { 256 final float eventX = event.getX(); 257 final float eventY = event.getY(); 258 if (!mMouseHoveringAtLeft && eventX == 0) { 259 mCallbacks.onMouseHoverAtLeft(); 260 mMouseHoveringAtLeft = true; 261 } else if (mMouseHoveringAtLeft && eventX > 0) { 262 mCallbacks.onMouseLeaveFromLeft(); 263 mMouseHoveringAtLeft = false; 264 } 265 if (!mMouseHoveringAtTop && eventY == 0) { 266 mCallbacks.onMouseHoverAtTop(); 267 mMouseHoveringAtTop = true; 268 } else if (mMouseHoveringAtTop && eventY > 0) { 269 mCallbacks.onMouseLeaveFromTop(); 270 mMouseHoveringAtTop = false; 271 } 272 if (!mMouseHoveringAtRight && eventX >= screenWidth - 1) { 273 mCallbacks.onMouseHoverAtRight(); 274 mMouseHoveringAtRight = true; 275 } else if (mMouseHoveringAtRight && eventX < screenWidth - 1) { 276 mCallbacks.onMouseLeaveFromRight(); 277 mMouseHoveringAtRight = false; 278 } 279 if (!mMouseHoveringAtBottom && eventY >= screenHeight - 1) { 280 mCallbacks.onMouseHoverAtBottom(); 281 mMouseHoveringAtBottom = true; 282 } else if (mMouseHoveringAtBottom && eventY < screenHeight - 1) { 283 mCallbacks.onMouseLeaveFromBottom(); 284 mMouseHoveringAtBottom = false; 285 } 286 } 287 break; 288 case MotionEvent.ACTION_UP: 289 case MotionEvent.ACTION_CANCEL: 290 mSwipeFireable = false; 291 mDebugFireable = false; 292 mCallbacks.onUpOrCancel(); 293 break; 294 default: 295 if (DEBUG) Slog.d(TAG, "Ignoring " + event); 296 } 297 } 298 captureDown(MotionEvent event, int pointerIndex)299 private void captureDown(MotionEvent event, int pointerIndex) { 300 final int pointerId = event.getPointerId(pointerIndex); 301 final int i = findIndex(pointerId); 302 if (DEBUG) Slog.d(TAG, "pointer " + pointerId 303 + " down pointerIndex=" + pointerIndex + " trackingIndex=" + i); 304 if (i != UNTRACKED_POINTER) { 305 mDownX[i] = event.getX(pointerIndex); 306 mDownY[i] = event.getY(pointerIndex); 307 mDownTime[i] = event.getEventTime(); 308 if (DEBUG) Slog.d(TAG, "pointer " + pointerId 309 + " down x=" + mDownX[i] + " y=" + mDownY[i]); 310 } 311 } 312 currentGestureStartedInRegion(Region r)313 protected boolean currentGestureStartedInRegion(Region r) { 314 return r.contains((int) mDownX[0], (int) mDownY[0]); 315 } 316 findIndex(int pointerId)317 private int findIndex(int pointerId) { 318 for (int i = 0; i < mDownPointers; i++) { 319 if (mDownPointerId[i] == pointerId) { 320 return i; 321 } 322 } 323 if (mDownPointers == MAX_TRACKED_POINTERS || pointerId == MotionEvent.INVALID_POINTER_ID) { 324 return UNTRACKED_POINTER; 325 } 326 mDownPointerId[mDownPointers++] = pointerId; 327 return mDownPointers - 1; 328 } 329 detectTrackpadThreeFingerSwipe(MotionEvent move)330 private int detectTrackpadThreeFingerSwipe(MotionEvent move) { 331 if (!isTrackpadThreeFingerSwipe(move)) { 332 return TRACKPAD_SWIPE_NONE; 333 } 334 335 float dx = move.getX() - mDownX[0]; 336 float dy = move.getY() - mDownY[0]; 337 if (Math.abs(dx) < Math.abs(dy)) { 338 if (Math.abs(dy) > mSwipeDistanceThreshold) { 339 return dy > 0 ? TRACKPAD_SWIPE_FROM_TOP : TRACKPAD_SWIPE_FROM_BOTTOM; 340 } 341 } else { 342 if (Math.abs(dx) > mSwipeDistanceThreshold) { 343 return dx > 0 ? TRACKPAD_SWIPE_FROM_LEFT : TRACKPAD_SWIPE_FROM_RIGHT; 344 } 345 } 346 347 return TRACKPAD_SWIPE_NONE; 348 } 349 isTrackpadThreeFingerSwipe(MotionEvent event)350 private static boolean isTrackpadThreeFingerSwipe(MotionEvent event) { 351 return event.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE 352 && event.getAxisValue(AXIS_GESTURE_SWIPE_FINGER_COUNT) == 3; 353 } 354 detectSwipe(MotionEvent move)355 private int detectSwipe(MotionEvent move) { 356 final int historySize = move.getHistorySize(); 357 final int pointerCount = move.getPointerCount(); 358 for (int p = 0; p < pointerCount; p++) { 359 final int pointerId = move.getPointerId(p); 360 final int i = findIndex(pointerId); 361 if (i != UNTRACKED_POINTER) { 362 for (int h = 0; h < historySize; h++) { 363 final long time = move.getHistoricalEventTime(h); 364 final float x = move.getHistoricalX(p, h); 365 final float y = move.getHistoricalY(p, h); 366 final int swipe = detectSwipe(i, time, x, y); 367 if (swipe != SWIPE_NONE) { 368 return swipe; 369 } 370 } 371 final int swipe = detectSwipe(i, move.getEventTime(), move.getX(p), move.getY(p)); 372 if (swipe != SWIPE_NONE) { 373 return swipe; 374 } 375 } 376 } 377 return SWIPE_NONE; 378 } 379 detectSwipe(int i, long time, float x, float y)380 private int detectSwipe(int i, long time, float x, float y) { 381 final float fromX = mDownX[i]; 382 final float fromY = mDownY[i]; 383 final long elapsed = time - mDownTime[i]; 384 if (DEBUG) Slog.d(TAG, "pointer " + mDownPointerId[i] 385 + " moved (" + fromX + "->" + x + "," + fromY + "->" + y + ") in " + elapsed); 386 if (fromY <= mSwipeStartThreshold.top 387 && y > fromY + mSwipeDistanceThreshold 388 && elapsed < SWIPE_TIMEOUT_MS) { 389 return SWIPE_FROM_TOP; 390 } 391 if (fromY >= screenHeight - mSwipeStartThreshold.bottom 392 && y < fromY - mSwipeDistanceThreshold 393 && elapsed < SWIPE_TIMEOUT_MS) { 394 return SWIPE_FROM_BOTTOM; 395 } 396 if (fromX >= screenWidth - mSwipeStartThreshold.right 397 && x < fromX - mSwipeDistanceThreshold 398 && elapsed < SWIPE_TIMEOUT_MS) { 399 return SWIPE_FROM_RIGHT; 400 } 401 if (fromX <= mSwipeStartThreshold.left 402 && x > fromX + mSwipeDistanceThreshold 403 && elapsed < SWIPE_TIMEOUT_MS) { 404 return SWIPE_FROM_LEFT; 405 } 406 return SWIPE_NONE; 407 } 408 dump(@onNull PrintWriter pw, @NonNull String prefix)409 public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { 410 final String inner = prefix + " "; 411 pw.println(prefix + TAG + ":"); 412 pw.print(inner); pw.print("mDisplayCutoutTouchableRegionSize="); 413 pw.println(mDisplayCutoutTouchableRegionSize); 414 pw.print(inner); pw.print("mSwipeStartThreshold="); pw.println(mSwipeStartThreshold); 415 pw.print(inner); pw.print("mSwipeDistanceThreshold="); pw.println(mSwipeDistanceThreshold); 416 } 417 418 private final class FlingGestureDetector extends GestureDetector.SimpleOnGestureListener { 419 420 private OverScroller mOverscroller; 421 FlingGestureDetector()422 FlingGestureDetector() { 423 mOverscroller = new OverScroller(mContext); 424 } 425 426 @Override onSingleTapUp(MotionEvent e)427 public boolean onSingleTapUp(MotionEvent e) { 428 if (!mOverscroller.isFinished()) { 429 mOverscroller.forceFinished(true); 430 } 431 return true; 432 } 433 @Override onFling(MotionEvent down, MotionEvent up, float velocityX, float velocityY)434 public boolean onFling(MotionEvent down, MotionEvent up, 435 float velocityX, float velocityY) { 436 mOverscroller.computeScrollOffset(); 437 long now = SystemClock.uptimeMillis(); 438 439 if (mLastFlingTime != 0 && now > mLastFlingTime + MAX_FLING_TIME_MILLIS) { 440 mOverscroller.forceFinished(true); 441 } 442 mOverscroller.fling(0, 0, (int)velocityX, (int)velocityY, 443 Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); 444 int duration = mOverscroller.getDuration(); 445 if (duration > MAX_FLING_TIME_MILLIS) { 446 duration = MAX_FLING_TIME_MILLIS; 447 } 448 mLastFlingTime = now; 449 mCallbacks.onFling(duration); 450 return true; 451 } 452 } 453 454 interface Callbacks { 455 void onSwipeFromTop(); 456 void onSwipeFromBottom(); 457 void onSwipeFromRight(); 458 void onSwipeFromLeft(); 459 void onFling(int durationMs); 460 void onDown(); 461 void onUpOrCancel(); 462 void onMouseHoverAtLeft(); 463 void onMouseHoverAtTop(); 464 void onMouseHoverAtRight(); 465 void onMouseHoverAtBottom(); 466 void onMouseLeaveFromLeft(); 467 void onMouseLeaveFromTop(); 468 void onMouseLeaveFromRight(); 469 void onMouseLeaveFromBottom(); 470 void onDebug(); 471 } 472 } 473