1 /* <lambda>null2 * Copyright (C) 2023 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.gesture 17 18 import android.content.Context 19 import android.graphics.Rect 20 import android.graphics.Region 21 import android.hardware.display.DisplayManagerGlobal 22 import android.os.Handler 23 import android.os.Looper 24 import android.os.SystemClock 25 import android.util.Log 26 import android.view.DisplayCutout 27 import android.view.DisplayInfo 28 import android.view.GestureDetector 29 import android.view.InputDevice 30 import android.view.InputEvent 31 import android.view.MotionEvent 32 import android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT 33 import android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE 34 import android.view.ViewRootImpl.CLIENT_TRANSIENT 35 import android.widget.OverScroller 36 import com.android.internal.R 37 import com.android.systemui.CoreStartable 38 import java.io.PrintWriter 39 import javax.inject.Inject 40 41 /** 42 * Watches for gesture events that may trigger system bar related events and notify the registered 43 * callbacks. Add callback to this listener by calling {@link setCallbacks}. 44 */ 45 class GesturePointerEventListener 46 @Inject 47 constructor(context: Context, gestureDetector: GesturePointerEventDetector) : CoreStartable { 48 private val mContext: Context 49 private val mHandler = Handler(Looper.getMainLooper()) 50 private var mGestureDetector: GesturePointerEventDetector 51 private var mFlingGestureDetector: GestureDetector? = null 52 private var mDisplayCutoutTouchableRegionSize = 0 53 54 // The thresholds for each edge of the display 55 private val mSwipeStartThreshold = Rect() 56 private var mSwipeDistanceThreshold = 0 57 private var mCallbacks: Callbacks? = null 58 private val mDownPointerId = IntArray(MAX_TRACKED_POINTERS) 59 private val mDownX = FloatArray(MAX_TRACKED_POINTERS) 60 private val mDownY = FloatArray(MAX_TRACKED_POINTERS) 61 private val mDownTime = LongArray(MAX_TRACKED_POINTERS) 62 var screenHeight = 0 63 var screenWidth = 0 64 private var mDownPointers = 0 65 private var mSwipeFireable = false 66 private var mDebugFireable = false 67 private var mMouseHoveringAtLeft = false 68 private var mMouseHoveringAtTop = false 69 private var mMouseHoveringAtRight = false 70 private var mMouseHoveringAtBottom = false 71 private var mLastFlingTime: Long = 0 72 73 init { 74 mContext = checkNull("context", context) 75 mGestureDetector = checkNull("gesture detector", gestureDetector) 76 onConfigurationChanged() 77 } 78 79 override fun start() { 80 if (!CLIENT_TRANSIENT) { 81 return 82 } 83 mGestureDetector.addOnGestureDetectedCallback(TAG) { ev -> onInputEvent(ev) } 84 mGestureDetector.startGestureListening() 85 86 mFlingGestureDetector = 87 object : GestureDetector(mContext, FlingGestureDetector(), mHandler) {} 88 } 89 90 fun onDisplayInfoChanged(info: DisplayInfo) { 91 screenWidth = info.logicalWidth 92 screenHeight = info.logicalHeight 93 onConfigurationChanged() 94 } 95 96 fun onConfigurationChanged() { 97 if (!CLIENT_TRANSIENT) { 98 return 99 } 100 val r = mContext.resources 101 val startThreshold = r.getDimensionPixelSize(R.dimen.system_gestures_start_threshold) 102 mSwipeStartThreshold[startThreshold, startThreshold, startThreshold] = startThreshold 103 mSwipeDistanceThreshold = 104 r.getDimensionPixelSize(R.dimen.system_gestures_distance_threshold) 105 val display = DisplayManagerGlobal.getInstance().getRealDisplay(mContext.displayId) 106 val displayCutout = display.cutout 107 if (displayCutout != null) { 108 // Expand swipe start threshold such that we can catch touches that just start beyond 109 // the notch area 110 mDisplayCutoutTouchableRegionSize = 111 r.getDimensionPixelSize(R.dimen.display_cutout_touchable_region_size) 112 val bounds = displayCutout.boundingRectsAll 113 if (bounds[DisplayCutout.BOUNDS_POSITION_LEFT] != null) { 114 mSwipeStartThreshold.left = 115 Math.max( 116 mSwipeStartThreshold.left, 117 bounds[DisplayCutout.BOUNDS_POSITION_LEFT]!!.width() + 118 mDisplayCutoutTouchableRegionSize 119 ) 120 } 121 if (bounds[DisplayCutout.BOUNDS_POSITION_TOP] != null) { 122 mSwipeStartThreshold.top = 123 Math.max( 124 mSwipeStartThreshold.top, 125 bounds[DisplayCutout.BOUNDS_POSITION_TOP]!!.height() + 126 mDisplayCutoutTouchableRegionSize 127 ) 128 } 129 if (bounds[DisplayCutout.BOUNDS_POSITION_RIGHT] != null) { 130 mSwipeStartThreshold.right = 131 Math.max( 132 mSwipeStartThreshold.right, 133 bounds[DisplayCutout.BOUNDS_POSITION_RIGHT]!!.width() + 134 mDisplayCutoutTouchableRegionSize 135 ) 136 } 137 if (bounds[DisplayCutout.BOUNDS_POSITION_BOTTOM] != null) { 138 mSwipeStartThreshold.bottom = 139 Math.max( 140 mSwipeStartThreshold.bottom, 141 bounds[DisplayCutout.BOUNDS_POSITION_BOTTOM]!!.height() + 142 mDisplayCutoutTouchableRegionSize 143 ) 144 } 145 } 146 if (DEBUG) 147 Log.d( 148 TAG, 149 "mSwipeStartThreshold=$mSwipeStartThreshold" + 150 " mSwipeDistanceThreshold=$mSwipeDistanceThreshold" 151 ) 152 } 153 154 fun onInputEvent(ev: InputEvent) { 155 if (ev !is MotionEvent) { 156 return 157 } 158 if (DEBUG) Log.d(TAG, "Received motion event $ev") 159 if (ev.isTouchEvent) { 160 mFlingGestureDetector?.onTouchEvent(ev) 161 } 162 when (ev.actionMasked) { 163 MotionEvent.ACTION_DOWN -> { 164 mSwipeFireable = true 165 mDebugFireable = true 166 mDownPointers = 0 167 captureDown(ev, 0) 168 if (mMouseHoveringAtLeft) { 169 mMouseHoveringAtLeft = false 170 mCallbacks?.onMouseLeaveFromLeft() 171 } 172 if (mMouseHoveringAtTop) { 173 mMouseHoveringAtTop = false 174 mCallbacks?.onMouseLeaveFromTop() 175 } 176 if (mMouseHoveringAtRight) { 177 mMouseHoveringAtRight = false 178 mCallbacks?.onMouseLeaveFromRight() 179 } 180 if (mMouseHoveringAtBottom) { 181 mMouseHoveringAtBottom = false 182 mCallbacks?.onMouseLeaveFromBottom() 183 } 184 mCallbacks?.onDown() 185 } 186 MotionEvent.ACTION_POINTER_DOWN -> { 187 captureDown(ev, ev.actionIndex) 188 if (mDebugFireable) { 189 mDebugFireable = ev.pointerCount < 5 190 if (!mDebugFireable) { 191 if (DEBUG) Log.d(TAG, "Firing debug") 192 mCallbacks?.onDebug() 193 } 194 } 195 } 196 MotionEvent.ACTION_MOVE -> 197 if (mSwipeFireable) { 198 val trackpadSwipe = detectTrackpadThreeFingerSwipe(ev) 199 mSwipeFireable = trackpadSwipe == TRACKPAD_SWIPE_NONE 200 if (!mSwipeFireable) { 201 if (trackpadSwipe == TRACKPAD_SWIPE_FROM_TOP) { 202 if (DEBUG) Log.d(TAG, "Firing onSwipeFromTop from trackpad") 203 mCallbacks?.onSwipeFromTop() 204 } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_BOTTOM) { 205 if (DEBUG) Log.d(TAG, "Firing onSwipeFromBottom from trackpad") 206 mCallbacks?.onSwipeFromBottom() 207 } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_RIGHT) { 208 if (DEBUG) Log.d(TAG, "Firing onSwipeFromRight from trackpad") 209 mCallbacks?.onSwipeFromRight() 210 } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_LEFT) { 211 if (DEBUG) Log.d(TAG, "Firing onSwipeFromLeft from trackpad") 212 mCallbacks?.onSwipeFromLeft() 213 } 214 } else { 215 val swipe = detectSwipe(ev) 216 mSwipeFireable = swipe == SWIPE_NONE 217 if (swipe == SWIPE_FROM_TOP) { 218 if (DEBUG) Log.d(TAG, "Firing onSwipeFromTop") 219 mCallbacks?.onSwipeFromTop() 220 } else if (swipe == SWIPE_FROM_BOTTOM) { 221 if (DEBUG) Log.d(TAG, "Firing onSwipeFromBottom") 222 mCallbacks?.onSwipeFromBottom() 223 } else if (swipe == SWIPE_FROM_RIGHT) { 224 if (DEBUG) Log.d(TAG, "Firing onSwipeFromRight") 225 mCallbacks?.onSwipeFromRight() 226 } else if (swipe == SWIPE_FROM_LEFT) { 227 if (DEBUG) Log.d(TAG, "Firing onSwipeFromLeft") 228 mCallbacks?.onSwipeFromLeft() 229 } 230 } 231 } 232 MotionEvent.ACTION_HOVER_MOVE -> 233 if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) { 234 val eventX = ev.x 235 val eventY = ev.y 236 if (!mMouseHoveringAtLeft && eventX == 0f) { 237 mCallbacks?.onMouseHoverAtLeft() 238 mMouseHoveringAtLeft = true 239 } else if (mMouseHoveringAtLeft && eventX > 0) { 240 mCallbacks?.onMouseLeaveFromLeft() 241 mMouseHoveringAtLeft = false 242 } 243 if (!mMouseHoveringAtTop && eventY == 0f) { 244 mCallbacks?.onMouseHoverAtTop() 245 mMouseHoveringAtTop = true 246 } else if (mMouseHoveringAtTop && eventY > 0) { 247 mCallbacks?.onMouseLeaveFromTop() 248 mMouseHoveringAtTop = false 249 } 250 if (!mMouseHoveringAtRight && eventX >= screenWidth - 1) { 251 mCallbacks?.onMouseHoverAtRight() 252 mMouseHoveringAtRight = true 253 } else if (mMouseHoveringAtRight && eventX < screenWidth - 1) { 254 mCallbacks?.onMouseLeaveFromRight() 255 mMouseHoveringAtRight = false 256 } 257 if (!mMouseHoveringAtBottom && eventY >= screenHeight - 1) { 258 mCallbacks?.onMouseHoverAtBottom() 259 mMouseHoveringAtBottom = true 260 } else if (mMouseHoveringAtBottom && eventY < screenHeight - 1) { 261 mCallbacks?.onMouseLeaveFromBottom() 262 mMouseHoveringAtBottom = false 263 } 264 } 265 MotionEvent.ACTION_UP, 266 MotionEvent.ACTION_CANCEL -> { 267 mSwipeFireable = false 268 mDebugFireable = false 269 mCallbacks?.onUpOrCancel() 270 } 271 else -> if (DEBUG) Log.d(TAG, "Ignoring $ev") 272 } 273 } 274 275 fun setCallbacks(callbacks: Callbacks) { 276 mCallbacks = callbacks 277 } 278 279 private fun captureDown(event: MotionEvent, pointerIndex: Int) { 280 val pointerId = event.getPointerId(pointerIndex) 281 val i = findIndex(pointerId) 282 if (DEBUG) Log.d(TAG, "pointer $pointerId down pointerIndex=$pointerIndex trackingIndex=$i") 283 if (i != UNTRACKED_POINTER) { 284 mDownX[i] = event.getX(pointerIndex) 285 mDownY[i] = event.getY(pointerIndex) 286 mDownTime[i] = event.eventTime 287 if (DEBUG) 288 Log.d(TAG, "pointer " + pointerId + " down x=" + mDownX[i] + " y=" + mDownY[i]) 289 } 290 } 291 292 protected fun currentGestureStartedInRegion(r: Region): Boolean { 293 return r.contains(mDownX[0].toInt(), mDownY[0].toInt()) 294 } 295 296 private fun findIndex(pointerId: Int): Int { 297 for (i in 0 until mDownPointers) { 298 if (mDownPointerId[i] == pointerId) { 299 return i 300 } 301 } 302 if (mDownPointers == MAX_TRACKED_POINTERS || pointerId == MotionEvent.INVALID_POINTER_ID) { 303 return UNTRACKED_POINTER 304 } 305 mDownPointerId[mDownPointers++] = pointerId 306 return mDownPointers - 1 307 } 308 309 private fun detectTrackpadThreeFingerSwipe(move: MotionEvent): Int { 310 if (!isTrackpadThreeFingerSwipe(move)) { 311 return TRACKPAD_SWIPE_NONE 312 } 313 val dx = move.x - mDownX[0] 314 val dy = move.y - mDownY[0] 315 if (Math.abs(dx) < Math.abs(dy)) { 316 if (Math.abs(dy) > mSwipeDistanceThreshold) { 317 return if (dy > 0) TRACKPAD_SWIPE_FROM_TOP else TRACKPAD_SWIPE_FROM_BOTTOM 318 } 319 } else { 320 if (Math.abs(dx) > mSwipeDistanceThreshold) { 321 return if (dx > 0) TRACKPAD_SWIPE_FROM_LEFT else TRACKPAD_SWIPE_FROM_RIGHT 322 } 323 } 324 return TRACKPAD_SWIPE_NONE 325 } 326 327 private fun isTrackpadThreeFingerSwipe(event: MotionEvent): Boolean { 328 return (event.classification == CLASSIFICATION_MULTI_FINGER_SWIPE && 329 event.getAxisValue(AXIS_GESTURE_SWIPE_FINGER_COUNT) == 3f) 330 } 331 private fun detectSwipe(move: MotionEvent): Int { 332 val historySize = move.historySize 333 val pointerCount = move.pointerCount 334 for (p in 0 until pointerCount) { 335 val pointerId = move.getPointerId(p) 336 val i = findIndex(pointerId) 337 if (i != UNTRACKED_POINTER) { 338 for (h in 0 until historySize) { 339 val time = move.getHistoricalEventTime(h) 340 val x = move.getHistoricalX(p, h) 341 val y = move.getHistoricalY(p, h) 342 val swipe = detectSwipe(i, time, x, y) 343 if (swipe != SWIPE_NONE) { 344 return swipe 345 } 346 } 347 val swipe = detectSwipe(i, move.eventTime, move.getX(p), move.getY(p)) 348 if (swipe != SWIPE_NONE) { 349 return swipe 350 } 351 } 352 } 353 return SWIPE_NONE 354 } 355 356 private fun detectSwipe(i: Int, time: Long, x: Float, y: Float): Int { 357 val fromX = mDownX[i] 358 val fromY = mDownY[i] 359 val elapsed = time - mDownTime[i] 360 if (DEBUG) 361 Log.d( 362 TAG, 363 "pointer " + 364 mDownPointerId[i] + 365 " moved (" + 366 fromX + 367 "->" + 368 x + 369 "," + 370 fromY + 371 "->" + 372 y + 373 ") in " + 374 elapsed 375 ) 376 if ( 377 fromY <= mSwipeStartThreshold.top && 378 y > fromY + mSwipeDistanceThreshold && 379 elapsed < SWIPE_TIMEOUT_MS 380 ) { 381 return SWIPE_FROM_TOP 382 } 383 if ( 384 fromY >= screenHeight - mSwipeStartThreshold.bottom && 385 y < fromY - mSwipeDistanceThreshold && 386 elapsed < SWIPE_TIMEOUT_MS 387 ) { 388 return SWIPE_FROM_BOTTOM 389 } 390 if ( 391 fromX >= screenWidth - mSwipeStartThreshold.right && 392 x < fromX - mSwipeDistanceThreshold && 393 elapsed < SWIPE_TIMEOUT_MS 394 ) { 395 return SWIPE_FROM_RIGHT 396 } 397 return if ( 398 fromX <= mSwipeStartThreshold.left && 399 x > fromX + mSwipeDistanceThreshold && 400 elapsed < SWIPE_TIMEOUT_MS 401 ) { 402 SWIPE_FROM_LEFT 403 } else SWIPE_NONE 404 } 405 406 fun dump(pw: PrintWriter, prefix: String) { 407 val inner = "$prefix " 408 pw.println(prefix + TAG + ":") 409 pw.print(inner) 410 pw.print("mDisplayCutoutTouchableRegionSize=") 411 pw.println(mDisplayCutoutTouchableRegionSize) 412 pw.print(inner) 413 pw.print("mSwipeStartThreshold=") 414 pw.println(mSwipeStartThreshold) 415 pw.print(inner) 416 pw.print("mSwipeDistanceThreshold=") 417 pw.println(mSwipeDistanceThreshold) 418 } 419 420 private inner class FlingGestureDetector internal constructor() : 421 GestureDetector.SimpleOnGestureListener() { 422 private val mOverscroller: OverScroller = OverScroller(mContext) 423 424 override fun onSingleTapUp(e: MotionEvent): Boolean { 425 if (!mOverscroller.isFinished) { 426 mOverscroller.forceFinished(true) 427 } 428 return true 429 } 430 431 override fun onFling( 432 down: MotionEvent?, 433 up: MotionEvent, 434 velocityX: Float, 435 velocityY: Float 436 ): Boolean { 437 mOverscroller.computeScrollOffset() 438 val now = SystemClock.uptimeMillis() 439 if (mLastFlingTime != 0L && now > mLastFlingTime + MAX_FLING_TIME_MILLIS) { 440 mOverscroller.forceFinished(true) 441 } 442 mOverscroller.fling( 443 0, 444 0, 445 velocityX.toInt(), 446 velocityY.toInt(), 447 Int.MIN_VALUE, 448 Int.MAX_VALUE, 449 Int.MIN_VALUE, 450 Int.MAX_VALUE 451 ) 452 var duration = mOverscroller.duration 453 if (duration > MAX_FLING_TIME_MILLIS) { 454 duration = MAX_FLING_TIME_MILLIS 455 } 456 mLastFlingTime = now 457 mCallbacks?.onFling(duration) 458 return true 459 } 460 } 461 462 interface Callbacks { 463 fun onSwipeFromTop() 464 fun onSwipeFromBottom() 465 fun onSwipeFromRight() 466 fun onSwipeFromLeft() 467 fun onFling(durationMs: Int) 468 fun onDown() 469 fun onUpOrCancel() 470 fun onMouseHoverAtLeft() 471 fun onMouseHoverAtTop() 472 fun onMouseHoverAtRight() 473 fun onMouseHoverAtBottom() 474 fun onMouseLeaveFromLeft() 475 fun onMouseLeaveFromTop() 476 fun onMouseLeaveFromRight() 477 fun onMouseLeaveFromBottom() 478 fun onDebug() 479 } 480 481 companion object { 482 private const val TAG = "GesturePointerEventHandler" 483 private const val DEBUG = false 484 private const val SWIPE_TIMEOUT_MS: Long = 500 485 private const val MAX_TRACKED_POINTERS = 32 // max per input system 486 private const val UNTRACKED_POINTER = -1 487 private const val MAX_FLING_TIME_MILLIS = 5000 488 private const val SWIPE_NONE = 0 489 private const val SWIPE_FROM_TOP = 1 490 private const val SWIPE_FROM_BOTTOM = 2 491 private const val SWIPE_FROM_RIGHT = 3 492 private const val SWIPE_FROM_LEFT = 4 493 private const val TRACKPAD_SWIPE_NONE = 0 494 private const val TRACKPAD_SWIPE_FROM_TOP = 1 495 private const val TRACKPAD_SWIPE_FROM_BOTTOM = 2 496 private const val TRACKPAD_SWIPE_FROM_RIGHT = 3 497 private const val TRACKPAD_SWIPE_FROM_LEFT = 4 498 499 private fun <T> checkNull(name: String, arg: T?): T { 500 requireNotNull(arg) { "$name must not be null" } 501 return arg 502 } 503 } 504 } 505