Introduce GesturePointerEventListener on sysUI (3/n)
This CL introduces the transient gesture detection logic on the client
side.
The GesturePointerEventHandler is a mirror of existing
SystemGesturesPointerEventListener on the window manager side. The
window manager one can be removed after the entire system UI side logic
is done.
There're some noticible differences, including:
1. GesturePointerEventListener uses system UI side GeneralGestureDetector
and a bridge class SystemGesturePointerEventListener to receive input
events.
2. Leave an interface to let the user, the event handler to register a
callback to receive necessary input events.
Test: build
Test: Enable debug and CLIENT_TRANSIENT flag to observe the log
Bug: 277290737
Change-Id: I13d98303b783dac072d1bd1d06f1b7f0836bf73d
diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt
index 006fc09..31df2ba 100644
--- a/packages/SystemUI/ktfmt_includes.txt
+++ b/packages/SystemUI/ktfmt_includes.txt
@@ -292,6 +292,7 @@
-packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
-packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
-packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt
+-packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventDetector.kt
-packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt
-packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt
-packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index d9665c5b5..aa0977a 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -49,6 +49,7 @@
import com.android.systemui.settings.dagger.MultiUserUtilsModule
import com.android.systemui.shortcut.ShortcutKeyDispatcher
import com.android.systemui.statusbar.ImmersiveModeConfirmation
+import com.android.systemui.statusbar.gesture.GesturePointerEventListener
import com.android.systemui.statusbar.notification.InstantAppNotifier
import com.android.systemui.statusbar.phone.KeyguardLiftController
import com.android.systemui.statusbar.phone.LockscreenWallpaper
@@ -181,6 +182,12 @@
@ClassKey(ScreenDecorations::class)
abstract fun bindScreenDecorations(sysui: ScreenDecorations): CoreStartable
+ /** Inject into GesturePointerEventHandler. */
+ @Binds
+ @IntoMap
+ @ClassKey(GesturePointerEventListener::class)
+ abstract fun bindGesturePointerEventListener(sysui: GesturePointerEventListener): CoreStartable
+
/** Inject into SessionTracker. */
@Binds
@IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventDetector.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventDetector.kt
new file mode 100644
index 0000000..b34c3ac
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventDetector.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.gesture
+
+import android.content.Context
+import android.view.InputEvent
+import android.view.MotionEvent
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.settings.DisplayTracker
+import javax.inject.Inject
+
+/**
+ * A class to detect when a motion event happens. To be notified when the event is detected, add a
+ * callback via [addOnGestureDetectedCallback].
+ */
+@SysUISingleton
+class GesturePointerEventDetector @Inject constructor(
+ private val context: Context,
+ displayTracker: DisplayTracker
+) : GenericGestureDetector(
+ GesturePointerEventDetector::class.simpleName!!,
+ displayTracker.defaultDisplayId
+) {
+ override fun onInputEvent(ev: InputEvent) {
+ if (ev !is MotionEvent) {
+ return
+ }
+ // Pass all events to [gestureDetector], which will then notify [gestureListener] when a tap
+ // is detected.
+ onGestureDetected(ev)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt
new file mode 100644
index 0000000..8505c5f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt
@@ -0,0 +1,504 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.gesture
+
+import android.content.Context
+import android.graphics.Rect
+import android.graphics.Region
+import android.hardware.display.DisplayManagerGlobal
+import android.os.Handler
+import android.os.Looper
+import android.os.SystemClock
+import android.util.Log
+import android.view.DisplayCutout
+import android.view.DisplayInfo
+import android.view.GestureDetector
+import android.view.InputDevice
+import android.view.InputEvent
+import android.view.MotionEvent
+import android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT
+import android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE
+import android.view.ViewRootImpl.CLIENT_TRANSIENT
+import android.widget.OverScroller
+import com.android.internal.R
+import com.android.systemui.CoreStartable
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * Watches for gesture events that may trigger system bar related events and notify the registered
+ * callbacks. Add callback to this listener by calling {@link setCallbacks}.
+ */
+class GesturePointerEventListener
+@Inject
+constructor(context: Context, gestureDetector: GesturePointerEventDetector) : CoreStartable {
+ private val mContext: Context
+ private val mHandler = Handler(Looper.getMainLooper())
+ private var mGestureDetector: GesturePointerEventDetector
+ private var mFlingGestureDetector: GestureDetector? = null
+ private var mDisplayCutoutTouchableRegionSize = 0
+
+ // The thresholds for each edge of the display
+ private val mSwipeStartThreshold = Rect()
+ private var mSwipeDistanceThreshold = 0
+ private var mCallbacks: Callbacks? = null
+ private val mDownPointerId = IntArray(MAX_TRACKED_POINTERS)
+ private val mDownX = FloatArray(MAX_TRACKED_POINTERS)
+ private val mDownY = FloatArray(MAX_TRACKED_POINTERS)
+ private val mDownTime = LongArray(MAX_TRACKED_POINTERS)
+ var screenHeight = 0
+ var screenWidth = 0
+ private var mDownPointers = 0
+ private var mSwipeFireable = false
+ private var mDebugFireable = false
+ private var mMouseHoveringAtLeft = false
+ private var mMouseHoveringAtTop = false
+ private var mMouseHoveringAtRight = false
+ private var mMouseHoveringAtBottom = false
+ private var mLastFlingTime: Long = 0
+
+ init {
+ mContext = checkNull("context", context)
+ mGestureDetector = checkNull("gesture detector", gestureDetector)
+ onConfigurationChanged()
+ }
+
+ override fun start() {
+ if (!CLIENT_TRANSIENT) {
+ return
+ }
+ mGestureDetector.addOnGestureDetectedCallback(TAG) { ev -> onInputEvent(ev) }
+ mGestureDetector.startGestureListening()
+
+ mFlingGestureDetector =
+ object : GestureDetector(mContext, FlingGestureDetector(), mHandler) {}
+ }
+
+ fun onDisplayInfoChanged(info: DisplayInfo) {
+ screenWidth = info.logicalWidth
+ screenHeight = info.logicalHeight
+ onConfigurationChanged()
+ }
+
+ fun onConfigurationChanged() {
+ if (!CLIENT_TRANSIENT) {
+ return
+ }
+ val r = mContext.resources
+ val defaultThreshold = r.getDimensionPixelSize(R.dimen.system_gestures_start_threshold)
+ mSwipeStartThreshold[defaultThreshold, defaultThreshold, defaultThreshold] =
+ defaultThreshold
+ mSwipeDistanceThreshold = defaultThreshold
+ val display = DisplayManagerGlobal.getInstance().getRealDisplay(mContext.displayId)
+ val displayCutout = display.cutout
+ if (displayCutout != null) {
+ // Expand swipe start threshold such that we can catch touches that just start beyond
+ // the notch area
+ mDisplayCutoutTouchableRegionSize =
+ r.getDimensionPixelSize(R.dimen.display_cutout_touchable_region_size)
+ val bounds = displayCutout.boundingRectsAll
+ if (bounds[DisplayCutout.BOUNDS_POSITION_LEFT] != null) {
+ mSwipeStartThreshold.left =
+ Math.max(
+ mSwipeStartThreshold.left,
+ bounds[DisplayCutout.BOUNDS_POSITION_LEFT]!!.width() +
+ mDisplayCutoutTouchableRegionSize
+ )
+ }
+ if (bounds[DisplayCutout.BOUNDS_POSITION_TOP] != null) {
+ mSwipeStartThreshold.top =
+ Math.max(
+ mSwipeStartThreshold.top,
+ bounds[DisplayCutout.BOUNDS_POSITION_TOP]!!.height() +
+ mDisplayCutoutTouchableRegionSize
+ )
+ }
+ if (bounds[DisplayCutout.BOUNDS_POSITION_RIGHT] != null) {
+ mSwipeStartThreshold.right =
+ Math.max(
+ mSwipeStartThreshold.right,
+ bounds[DisplayCutout.BOUNDS_POSITION_RIGHT]!!.width() +
+ mDisplayCutoutTouchableRegionSize
+ )
+ }
+ if (bounds[DisplayCutout.BOUNDS_POSITION_BOTTOM] != null) {
+ mSwipeStartThreshold.bottom =
+ Math.max(
+ mSwipeStartThreshold.bottom,
+ bounds[DisplayCutout.BOUNDS_POSITION_BOTTOM]!!.height() +
+ mDisplayCutoutTouchableRegionSize
+ )
+ }
+ }
+ if (DEBUG)
+ Log.d(
+ TAG,
+ "mSwipeStartThreshold=$mSwipeStartThreshold" +
+ " mSwipeDistanceThreshold=$mSwipeDistanceThreshold"
+ )
+ }
+
+ fun onInputEvent(ev: InputEvent) {
+ if (ev !is MotionEvent) {
+ return
+ }
+ if (DEBUG) Log.d(TAG, "Received motion event $ev")
+ if (ev.isTouchEvent) {
+ mFlingGestureDetector?.onTouchEvent(ev)
+ }
+ when (ev.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ mSwipeFireable = true
+ mDebugFireable = true
+ mDownPointers = 0
+ captureDown(ev, 0)
+ if (mMouseHoveringAtLeft) {
+ mMouseHoveringAtLeft = false
+ mCallbacks?.onMouseLeaveFromLeft()
+ }
+ if (mMouseHoveringAtTop) {
+ mMouseHoveringAtTop = false
+ mCallbacks?.onMouseLeaveFromTop()
+ }
+ if (mMouseHoveringAtRight) {
+ mMouseHoveringAtRight = false
+ mCallbacks?.onMouseLeaveFromRight()
+ }
+ if (mMouseHoveringAtBottom) {
+ mMouseHoveringAtBottom = false
+ mCallbacks?.onMouseLeaveFromBottom()
+ }
+ mCallbacks?.onDown()
+ }
+ MotionEvent.ACTION_POINTER_DOWN -> {
+ captureDown(ev, ev.actionIndex)
+ if (mDebugFireable) {
+ mDebugFireable = ev.pointerCount < 5
+ if (!mDebugFireable) {
+ if (DEBUG) Log.d(TAG, "Firing debug")
+ mCallbacks?.onDebug()
+ }
+ }
+ }
+ MotionEvent.ACTION_MOVE ->
+ if (mSwipeFireable) {
+ val trackpadSwipe = detectTrackpadThreeFingerSwipe(ev)
+ mSwipeFireable = trackpadSwipe == TRACKPAD_SWIPE_NONE
+ if (!mSwipeFireable) {
+ if (trackpadSwipe == TRACKPAD_SWIPE_FROM_TOP) {
+ if (DEBUG) Log.d(TAG, "Firing onSwipeFromTop from trackpad")
+ mCallbacks?.onSwipeFromTop()
+ } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_BOTTOM) {
+ if (DEBUG) Log.d(TAG, "Firing onSwipeFromBottom from trackpad")
+ mCallbacks?.onSwipeFromBottom()
+ } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_RIGHT) {
+ if (DEBUG) Log.d(TAG, "Firing onSwipeFromRight from trackpad")
+ mCallbacks?.onSwipeFromRight()
+ } else if (trackpadSwipe == TRACKPAD_SWIPE_FROM_LEFT) {
+ if (DEBUG) Log.d(TAG, "Firing onSwipeFromLeft from trackpad")
+ mCallbacks?.onSwipeFromLeft()
+ }
+ } else {
+ val swipe = detectSwipe(ev)
+ mSwipeFireable = swipe == SWIPE_NONE
+ if (swipe == SWIPE_FROM_TOP) {
+ if (DEBUG) Log.d(TAG, "Firing onSwipeFromTop")
+ mCallbacks?.onSwipeFromTop()
+ } else if (swipe == SWIPE_FROM_BOTTOM) {
+ if (DEBUG) Log.d(TAG, "Firing onSwipeFromBottom")
+ mCallbacks?.onSwipeFromBottom()
+ } else if (swipe == SWIPE_FROM_RIGHT) {
+ if (DEBUG) Log.d(TAG, "Firing onSwipeFromRight")
+ mCallbacks?.onSwipeFromRight()
+ } else if (swipe == SWIPE_FROM_LEFT) {
+ if (DEBUG) Log.d(TAG, "Firing onSwipeFromLeft")
+ mCallbacks?.onSwipeFromLeft()
+ }
+ }
+ }
+ MotionEvent.ACTION_HOVER_MOVE ->
+ if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) {
+ val eventX = ev.x
+ val eventY = ev.y
+ if (!mMouseHoveringAtLeft && eventX == 0f) {
+ mCallbacks?.onMouseHoverAtLeft()
+ mMouseHoveringAtLeft = true
+ } else if (mMouseHoveringAtLeft && eventX > 0) {
+ mCallbacks?.onMouseLeaveFromLeft()
+ mMouseHoveringAtLeft = false
+ }
+ if (!mMouseHoveringAtTop && eventY == 0f) {
+ mCallbacks?.onMouseHoverAtTop()
+ mMouseHoveringAtTop = true
+ } else if (mMouseHoveringAtTop && eventY > 0) {
+ mCallbacks?.onMouseLeaveFromTop()
+ mMouseHoveringAtTop = false
+ }
+ if (!mMouseHoveringAtRight && eventX >= screenWidth - 1) {
+ mCallbacks?.onMouseHoverAtRight()
+ mMouseHoveringAtRight = true
+ } else if (mMouseHoveringAtRight && eventX < screenWidth - 1) {
+ mCallbacks?.onMouseLeaveFromRight()
+ mMouseHoveringAtRight = false
+ }
+ if (!mMouseHoveringAtBottom && eventY >= screenHeight - 1) {
+ mCallbacks?.onMouseHoverAtBottom()
+ mMouseHoveringAtBottom = true
+ } else if (mMouseHoveringAtBottom && eventY < screenHeight - 1) {
+ mCallbacks?.onMouseLeaveFromBottom()
+ mMouseHoveringAtBottom = false
+ }
+ }
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_CANCEL -> {
+ mSwipeFireable = false
+ mDebugFireable = false
+ mCallbacks?.onUpOrCancel()
+ }
+ else -> if (DEBUG) Log.d(TAG, "Ignoring $ev")
+ }
+ }
+
+ fun setCallbacks(callbacks: Callbacks) {
+ mCallbacks = callbacks
+ }
+
+ private fun captureDown(event: MotionEvent, pointerIndex: Int) {
+ val pointerId = event.getPointerId(pointerIndex)
+ val i = findIndex(pointerId)
+ if (DEBUG) Log.d(TAG, "pointer $pointerId down pointerIndex=$pointerIndex trackingIndex=$i")
+ if (i != UNTRACKED_POINTER) {
+ mDownX[i] = event.getX(pointerIndex)
+ mDownY[i] = event.getY(pointerIndex)
+ mDownTime[i] = event.eventTime
+ if (DEBUG)
+ Log.d(TAG, "pointer " + pointerId + " down x=" + mDownX[i] + " y=" + mDownY[i])
+ }
+ }
+
+ protected fun currentGestureStartedInRegion(r: Region): Boolean {
+ return r.contains(mDownX[0].toInt(), mDownY[0].toInt())
+ }
+
+ private fun findIndex(pointerId: Int): Int {
+ for (i in 0 until mDownPointers) {
+ if (mDownPointerId[i] == pointerId) {
+ return i
+ }
+ }
+ if (mDownPointers == MAX_TRACKED_POINTERS || pointerId == MotionEvent.INVALID_POINTER_ID) {
+ return UNTRACKED_POINTER
+ }
+ mDownPointerId[mDownPointers++] = pointerId
+ return mDownPointers - 1
+ }
+
+ private fun detectTrackpadThreeFingerSwipe(move: MotionEvent): Int {
+ if (!isTrackpadThreeFingerSwipe(move)) {
+ return TRACKPAD_SWIPE_NONE
+ }
+ val dx = move.x - mDownX[0]
+ val dy = move.y - mDownY[0]
+ if (Math.abs(dx) < Math.abs(dy)) {
+ if (Math.abs(dy) > mSwipeDistanceThreshold) {
+ return if (dy > 0) TRACKPAD_SWIPE_FROM_TOP else TRACKPAD_SWIPE_FROM_BOTTOM
+ }
+ } else {
+ if (Math.abs(dx) > mSwipeDistanceThreshold) {
+ return if (dx > 0) TRACKPAD_SWIPE_FROM_LEFT else TRACKPAD_SWIPE_FROM_RIGHT
+ }
+ }
+ return TRACKPAD_SWIPE_NONE
+ }
+
+ private fun isTrackpadThreeFingerSwipe(event: MotionEvent): Boolean {
+ return (event.classification == CLASSIFICATION_MULTI_FINGER_SWIPE &&
+ event.getAxisValue(AXIS_GESTURE_SWIPE_FINGER_COUNT) == 3f)
+ }
+ private fun detectSwipe(move: MotionEvent): Int {
+ val historySize = move.historySize
+ val pointerCount = move.pointerCount
+ for (p in 0 until pointerCount) {
+ val pointerId = move.getPointerId(p)
+ val i = findIndex(pointerId)
+ if (i != UNTRACKED_POINTER) {
+ for (h in 0 until historySize) {
+ val time = move.getHistoricalEventTime(h)
+ val x = move.getHistoricalX(p, h)
+ val y = move.getHistoricalY(p, h)
+ val swipe = detectSwipe(i, time, x, y)
+ if (swipe != SWIPE_NONE) {
+ return swipe
+ }
+ }
+ val swipe = detectSwipe(i, move.eventTime, move.getX(p), move.getY(p))
+ if (swipe != SWIPE_NONE) {
+ return swipe
+ }
+ }
+ }
+ return SWIPE_NONE
+ }
+
+ private fun detectSwipe(i: Int, time: Long, x: Float, y: Float): Int {
+ val fromX = mDownX[i]
+ val fromY = mDownY[i]
+ val elapsed = time - mDownTime[i]
+ if (DEBUG)
+ Log.d(
+ TAG,
+ "pointer " +
+ mDownPointerId[i] +
+ " moved (" +
+ fromX +
+ "->" +
+ x +
+ "," +
+ fromY +
+ "->" +
+ y +
+ ") in " +
+ elapsed
+ )
+ if (
+ fromY <= mSwipeStartThreshold.top &&
+ y > fromY + mSwipeDistanceThreshold &&
+ elapsed < SWIPE_TIMEOUT_MS
+ ) {
+ return SWIPE_FROM_TOP
+ }
+ if (
+ fromY >= screenHeight - mSwipeStartThreshold.bottom &&
+ y < fromY - mSwipeDistanceThreshold &&
+ elapsed < SWIPE_TIMEOUT_MS
+ ) {
+ return SWIPE_FROM_BOTTOM
+ }
+ if (
+ fromX >= screenWidth - mSwipeStartThreshold.right &&
+ x < fromX - mSwipeDistanceThreshold &&
+ elapsed < SWIPE_TIMEOUT_MS
+ ) {
+ return SWIPE_FROM_RIGHT
+ }
+ return if (
+ fromX <= mSwipeStartThreshold.left &&
+ x > fromX + mSwipeDistanceThreshold &&
+ elapsed < SWIPE_TIMEOUT_MS
+ ) {
+ SWIPE_FROM_LEFT
+ } else SWIPE_NONE
+ }
+
+ fun dump(pw: PrintWriter, prefix: String) {
+ val inner = "$prefix "
+ pw.println(prefix + TAG + ":")
+ pw.print(inner)
+ pw.print("mDisplayCutoutTouchableRegionSize=")
+ pw.println(mDisplayCutoutTouchableRegionSize)
+ pw.print(inner)
+ pw.print("mSwipeStartThreshold=")
+ pw.println(mSwipeStartThreshold)
+ pw.print(inner)
+ pw.print("mSwipeDistanceThreshold=")
+ pw.println(mSwipeDistanceThreshold)
+ }
+
+ private inner class FlingGestureDetector internal constructor() :
+ GestureDetector.SimpleOnGestureListener() {
+ private val mOverscroller: OverScroller = OverScroller(mContext)
+
+ override fun onSingleTapUp(e: MotionEvent): Boolean {
+ if (!mOverscroller.isFinished) {
+ mOverscroller.forceFinished(true)
+ }
+ return true
+ }
+
+ override fun onFling(
+ down: MotionEvent?,
+ up: MotionEvent,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean {
+ mOverscroller.computeScrollOffset()
+ val now = SystemClock.uptimeMillis()
+ if (mLastFlingTime != 0L && now > mLastFlingTime + MAX_FLING_TIME_MILLIS) {
+ mOverscroller.forceFinished(true)
+ }
+ mOverscroller.fling(
+ 0,
+ 0,
+ velocityX.toInt(),
+ velocityY.toInt(),
+ Int.MIN_VALUE,
+ Int.MAX_VALUE,
+ Int.MIN_VALUE,
+ Int.MAX_VALUE
+ )
+ var duration = mOverscroller.duration
+ if (duration > MAX_FLING_TIME_MILLIS) {
+ duration = MAX_FLING_TIME_MILLIS
+ }
+ mLastFlingTime = now
+ mCallbacks?.onFling(duration)
+ return true
+ }
+ }
+
+ interface Callbacks {
+ fun onSwipeFromTop()
+ fun onSwipeFromBottom()
+ fun onSwipeFromRight()
+ fun onSwipeFromLeft()
+ fun onFling(durationMs: Int)
+ fun onDown()
+ fun onUpOrCancel()
+ fun onMouseHoverAtLeft()
+ fun onMouseHoverAtTop()
+ fun onMouseHoverAtRight()
+ fun onMouseHoverAtBottom()
+ fun onMouseLeaveFromLeft()
+ fun onMouseLeaveFromTop()
+ fun onMouseLeaveFromRight()
+ fun onMouseLeaveFromBottom()
+ fun onDebug()
+ }
+
+ companion object {
+ private const val TAG = "GesturePointerEventHandler"
+ private const val DEBUG = false
+ private const val SWIPE_TIMEOUT_MS: Long = 500
+ private const val MAX_TRACKED_POINTERS = 32 // max per input system
+ private const val UNTRACKED_POINTER = -1
+ private const val MAX_FLING_TIME_MILLIS = 5000
+ private const val SWIPE_NONE = 0
+ private const val SWIPE_FROM_TOP = 1
+ private const val SWIPE_FROM_BOTTOM = 2
+ private const val SWIPE_FROM_RIGHT = 3
+ private const val SWIPE_FROM_LEFT = 4
+ private const val TRACKPAD_SWIPE_NONE = 0
+ private const val TRACKPAD_SWIPE_FROM_TOP = 1
+ private const val TRACKPAD_SWIPE_FROM_BOTTOM = 2
+ private const val TRACKPAD_SWIPE_FROM_RIGHT = 3
+ private const val TRACKPAD_SWIPE_FROM_LEFT = 4
+
+ private fun <T> checkNull(name: String, arg: T?): T {
+ requireNotNull(arg) { "$name must not be null" }
+ return arg
+ }
+ }
+}