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