Merge changes Ia49455ed,Ib0b8d038 into main

* changes:
  Simplifying TouchpadGestureMonitor interface
  Refactoring recent app gesture recognition logic
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
index ba9fa92..cd18925 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
 import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -34,10 +35,12 @@
 
     private var gestureState: GestureState = NotStarted
     private val gestureMonitor =
-        BackGestureMonitor(
-            gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
-            gestureStateChangedCallback = { gestureState = it },
-        )
+        BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
+
+    @Before
+    fun before() {
+        gestureMonitor.addGestureStateCallback { gestureState = it }
+    }
 
     @Test
     fun triggersGestureFinishedForThreeFingerGestureRight() {
@@ -82,7 +85,7 @@
     }
 
     private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
-        events.forEach { gestureMonitor.processTouchpadEvent(it) }
+        events.forEach { gestureMonitor.accept(it) }
         assertThat(gestureState).isEqualTo(expectedState)
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
index a83ed56..3f1633a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
@@ -36,10 +36,7 @@
     private var triggered = false
     private val handler =
         TouchpadGestureHandler(
-            BackGestureMonitor(
-                gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
-                gestureStateChangedCallback = {},
-            ),
+            BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt()),
             EasterEggGestureMonitor(callback = { triggered = true }),
         )
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
index 59cc026..edf0e56 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
 import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -34,10 +35,12 @@
 
     private var gestureState: GestureState = GestureState.NotStarted
     private val gestureMonitor =
-        HomeGestureMonitor(
-            gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
-            gestureStateChangedCallback = { gestureState = it },
-        )
+        HomeGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
+
+    @Before
+    fun before() {
+        gestureMonitor.addGestureStateCallback { gestureState = it }
+    }
 
     @Test
     fun triggersGestureFinishedForThreeFingerGestureUp() {
@@ -78,7 +81,7 @@
     }
 
     private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
-        events.forEach { gestureMonitor.processTouchpadEvent(it) }
+        events.forEach { gestureMonitor.accept(it) }
         assertThat(gestureState).isEqualTo(expectedState)
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt
index 7eac6bb..f68e773 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
 import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.doReturn
@@ -44,7 +45,7 @@
     }
 
     private var gestureState: GestureState = GestureState.NotStarted
-    private val velocityTracker =
+    private val velocityTracker1D =
         mock<VelocityTracker1D> {
             // by default return correct speed for the gesture - as if pointer is slowing down
             on { calculateVelocity() } doReturn SLOW
@@ -52,11 +53,15 @@
     private val gestureMonitor =
         RecentAppsGestureMonitor(
             gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
-            gestureStateChangedCallback = { gestureState = it },
             velocityThresholdPxPerMs = THRESHOLD_VELOCITY_PX_PER_MS,
-            velocityTracker = velocityTracker,
+            velocityTracker = VerticalVelocityTracker(velocityTracker1D),
         )
 
+    @Before
+    fun before() {
+        gestureMonitor.addGestureStateCallback { gestureState = it }
+    }
+
     @Test
     fun triggersGestureFinishedForThreeFingerGestureUp() {
         assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = Finished)
@@ -64,7 +69,7 @@
 
     @Test
     fun doesntTriggerGestureFinished_onGestureSpeedTooHigh() {
-        whenever(velocityTracker.calculateVelocity()).thenReturn(FAST)
+        whenever(velocityTracker1D.calculateVelocity()).thenReturn(FAST)
         assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = NotStarted)
     }
 
@@ -102,7 +107,7 @@
     }
 
     private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
-        events.forEach { gestureMonitor.processTouchpadEvent(it) }
+        events.forEach { gestureMonitor.accept(it) }
         assertThat(gestureState).isEqualTo(expectedState)
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
index 4d26366..9f7ea679 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -35,14 +36,14 @@
 class TouchpadGestureHandlerTest : SysuiTestCase() {
 
     private var gestureState: GestureState = GestureState.NotStarted
-    private val handler =
-        TouchpadGestureHandler(
-            BackGestureMonitor(
-                gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
-                gestureStateChangedCallback = { gestureState = it },
-            ),
-            EasterEggGestureMonitor {},
-        )
+    private val gestureMonitor =
+        BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
+    private val handler = TouchpadGestureHandler(gestureMonitor, EasterEggGestureMonitor {})
+
+    @Before
+    fun before() {
+        gestureMonitor.addGestureStateCallback { gestureState = it }
+    }
 
     @Test
     fun handlesEventsFromTouchpad() {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
index bfc5429..6879a34 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
@@ -27,10 +27,7 @@
 import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureMonitor
 
 @Composable
-fun BackGestureTutorialScreen(
-    onDoneButtonClicked: () -> Unit,
-    onBack: () -> Unit,
-) {
+fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
     val screenConfig =
         TutorialScreenConfig(
             colors = rememberScreenColors(),
@@ -39,18 +36,20 @@
                     titleResId = R.string.touchpad_back_gesture_action_title,
                     bodyResId = R.string.touchpad_back_gesture_guidance,
                     titleSuccessResId = R.string.touchpad_back_gesture_success_title,
-                    bodySuccessResId = R.string.touchpad_back_gesture_success_body
+                    bodySuccessResId = R.string.touchpad_back_gesture_success_body,
                 ),
             animations =
                 TutorialScreenConfig.Animations(
                     educationResId = R.raw.trackpad_back_edu,
-                    successResId = R.raw.trackpad_back_success
-                )
+                    successResId = R.raw.trackpad_back_success,
+                ),
         )
     val gestureMonitorProvider =
         DistanceBasedGestureMonitorProvider(
             monitorFactory = { distanceThresholdPx, gestureStateCallback ->
-                BackGestureMonitor(distanceThresholdPx, gestureStateCallback)
+                BackGestureMonitor(distanceThresholdPx).also {
+                    it.addGestureStateCallback(gestureStateCallback)
+                }
             }
         )
     GestureTutorialScreen(screenConfig, gestureMonitorProvider, onDoneButtonClicked, onBack)
@@ -67,7 +66,7 @@
             rememberColorFilterProperty(".tertiaryFixedDim", tertiaryFixedDim),
             rememberColorFilterProperty(".onTertiaryFixed", onTertiaryFixed),
             rememberColorFilterProperty(".onTertiary", onTertiary),
-            rememberColorFilterProperty(".onTertiaryFixedVariant", onTertiaryFixedVariant)
+            rememberColorFilterProperty(".onTertiaryFixedVariant", onTertiaryFixedVariant),
         )
     val screenColors =
         remember(dynamicProperties) {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
index f2fec5f..a55fa44 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
@@ -26,10 +26,7 @@
 import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureMonitor
 
 @Composable
-fun HomeGestureTutorialScreen(
-    onDoneButtonClicked: () -> Unit,
-    onBack: () -> Unit,
-) {
+fun HomeGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
     val screenConfig =
         TutorialScreenConfig(
             colors = rememberScreenColors(),
@@ -38,18 +35,20 @@
                     titleResId = R.string.touchpad_home_gesture_action_title,
                     bodyResId = R.string.touchpad_home_gesture_guidance,
                     titleSuccessResId = R.string.touchpad_home_gesture_success_title,
-                    bodySuccessResId = R.string.touchpad_home_gesture_success_body
+                    bodySuccessResId = R.string.touchpad_home_gesture_success_body,
                 ),
             animations =
                 TutorialScreenConfig.Animations(
                     educationResId = R.raw.trackpad_home_edu,
-                    successResId = R.raw.trackpad_home_success
-                )
+                    successResId = R.raw.trackpad_home_success,
+                ),
         )
     val gestureMonitorProvider =
         DistanceBasedGestureMonitorProvider(
             monitorFactory = { distanceThresholdPx, gestureStateCallback ->
-                HomeGestureMonitor(distanceThresholdPx, gestureStateCallback)
+                HomeGestureMonitor(distanceThresholdPx).also {
+                    it.addGestureStateCallback(gestureStateCallback)
+                }
             }
         )
     GestureTutorialScreen(screenConfig, gestureMonitorProvider, onDoneButtonClicked, onBack)
@@ -64,7 +63,7 @@
         rememberLottieDynamicProperties(
             rememberColorFilterProperty(".primaryFixedDim", primaryFixedDim),
             rememberColorFilterProperty(".onPrimaryFixed", onPrimaryFixed),
-            rememberColorFilterProperty(".onPrimaryFixedVariant", onPrimaryFixedVariant)
+            rememberColorFilterProperty(".onPrimaryFixedVariant", onPrimaryFixedVariant),
         )
     val screenColors =
         remember(dynamicProperties) {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
index b2fb6cd..6ee15aa 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
@@ -29,10 +29,7 @@
 import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureMonitor
 
 @Composable
-fun RecentAppsGestureTutorialScreen(
-    onDoneButtonClicked: () -> Unit,
-    onBack: () -> Unit,
-) {
+fun RecentAppsGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
     val screenConfig =
         TutorialScreenConfig(
             colors = rememberScreenColors(),
@@ -41,20 +38,20 @@
                     titleResId = R.string.touchpad_recent_apps_gesture_action_title,
                     bodyResId = R.string.touchpad_recent_apps_gesture_guidance,
                     titleSuccessResId = R.string.touchpad_recent_apps_gesture_success_title,
-                    bodySuccessResId = R.string.touchpad_recent_apps_gesture_success_body
+                    bodySuccessResId = R.string.touchpad_recent_apps_gesture_success_body,
                 ),
             animations =
                 TutorialScreenConfig.Animations(
                     educationResId = R.raw.trackpad_recent_apps_edu,
-                    successResId = R.raw.trackpad_recent_apps_success
-                )
+                    successResId = R.raw.trackpad_recent_apps_success,
+                ),
         )
     val gestureMonitorProvider =
         object : GestureMonitorProvider {
             @Composable
             override fun rememberGestureMonitor(
                 resources: Resources,
-                gestureStateChangedCallback: (GestureState) -> Unit
+                gestureStateChangedCallback: (GestureState) -> Unit,
             ): TouchpadGestureMonitor {
                 val distanceThresholdPx =
                     resources.getDimensionPixelSize(
@@ -63,11 +60,9 @@
                 val velocityThresholdPxPerMs =
                     resources.getDimension(R.dimen.touchpad_recent_apps_gesture_velocity_threshold)
                 return remember(distanceThresholdPx, velocityThresholdPxPerMs) {
-                    RecentAppsGestureMonitor(
-                        distanceThresholdPx,
-                        gestureStateChangedCallback,
-                        velocityThresholdPxPerMs
-                    )
+                    RecentAppsGestureMonitor(distanceThresholdPx, velocityThresholdPxPerMs).also {
+                        it.addGestureStateCallback(gestureStateChangedCallback)
+                    }
                 }
             }
         }
@@ -83,7 +78,7 @@
         rememberLottieDynamicProperties(
             rememberColorFilterProperty(".secondaryFixedDim", secondaryFixedDim),
             rememberColorFilterProperty(".onSecondaryFixed", onSecondaryFixed),
-            rememberColorFilterProperty(".onSecondaryFixedVariant", onSecondaryFixedVariant)
+            rememberColorFilterProperty(".onSecondaryFixedVariant", onSecondaryFixedVariant),
         )
     val screenColors =
         remember(dynamicProperties) {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
index ecb5574..490f04d 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
@@ -20,18 +20,21 @@
 import kotlin.math.abs
 
 /** Monitors for touchpad back gesture, that is three fingers swiping left or right */
-class BackGestureMonitor(
-    private val gestureDistanceThresholdPx: Int,
-    override val gestureStateChangedCallback: (GestureState) -> Unit,
-) : TouchpadGestureMonitor {
-    private val distanceTracker = DistanceTracker()
+class BackGestureMonitor(private val gestureDistanceThresholdPx: Int) : TouchpadGestureMonitor {
 
-    override fun processTouchpadEvent(event: MotionEvent) {
+    private val distanceTracker = DistanceTracker()
+    private var gestureStateChangedCallback: (GestureState) -> Unit = {}
+
+    override fun addGestureStateCallback(callback: (GestureState) -> Unit) {
+        gestureStateChangedCallback = callback
+    }
+
+    override fun accept(event: MotionEvent) {
         if (!isThreeFingerTouchpadSwipe(event)) return
-        val distanceState = distanceTracker.processEvent(event)
-        updateGestureStateBasedOnDistance(
+        val gestureState = distanceTracker.processEvent(event)
+        updateGestureState(
             gestureStateChangedCallback,
-            distanceState,
+            gestureState,
             isFinished = { abs(it.deltaX) >= gestureDistanceThresholdPx },
             progress = { 0f },
         )
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt
index 70d9366..d482358 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt
@@ -38,10 +38,10 @@
     }
 }
 
-sealed interface DistanceGestureState
+sealed class DistanceGestureState(val deltaX: Float, val deltaY: Float)
 
-class Started(val deltaX: Float, val deltaY: Float) : DistanceGestureState
+class Started(deltaX: Float, deltaY: Float) : DistanceGestureState(deltaX, deltaY)
 
-class Moving(val deltaX: Float, val deltaY: Float) : DistanceGestureState
+class Moving(deltaX: Float, deltaY: Float) : DistanceGestureState(deltaX, deltaY)
 
-class Finished(val deltaX: Float, val deltaY: Float) : DistanceGestureState
+class Finished(deltaX: Float, deltaY: Float) : DistanceGestureState(deltaX, deltaY)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
index c1caeb3..f194677 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
@@ -16,11 +16,8 @@
 
 package com.android.systemui.touchpad.tutorial.ui.gesture
 
-/**
- * Helper function for gesture recognizers to have common state triggering logic based on distance
- * only.
- */
-inline fun updateGestureStateBasedOnDistance(
+/** Helper function for gesture recognizers to have common state triggering logic */
+inline fun updateGestureState(
     gestureStateChangedCallback: (GestureState) -> Unit,
     gestureState: DistanceGestureState?,
     isFinished: (Finished) -> Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
index fdcf9de..83d4f56 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
@@ -19,18 +19,21 @@
 import android.view.MotionEvent
 
 /** Monitors for touchpad home gesture, that is three fingers swiping up */
-class HomeGestureMonitor(
-    private val gestureDistanceThresholdPx: Int,
-    override val gestureStateChangedCallback: (GestureState) -> Unit,
-) : TouchpadGestureMonitor {
-    private val distanceTracker = DistanceTracker()
+class HomeGestureMonitor(private val gestureDistanceThresholdPx: Int) : TouchpadGestureMonitor {
 
-    override fun processTouchpadEvent(event: MotionEvent) {
+    private val distanceTracker = DistanceTracker()
+    private var gestureStateChangedCallback: (GestureState) -> Unit = {}
+
+    override fun addGestureStateCallback(callback: (GestureState) -> Unit) {
+        gestureStateChangedCallback = callback
+    }
+
+    override fun accept(event: MotionEvent) {
         if (!isThreeFingerTouchpadSwipe(event)) return
-        val distanceState = distanceTracker.processEvent(event)
-        updateGestureStateBasedOnDistance(
+        val gestureState = distanceTracker.processEvent(event)
+        updateGestureState(
             gestureStateChangedCallback,
-            distanceState,
+            gestureState,
             isFinished = { -it.deltaY >= gestureDistanceThresholdPx },
             progress = { 0f },
         )
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt
index dd31ce3..1731bb8 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.touchpad.tutorial.ui.gesture
 
 import android.view.MotionEvent
-import androidx.compose.ui.input.pointer.util.VelocityTracker1D
 import kotlin.math.abs
 
 /**
@@ -27,45 +26,30 @@
  */
 class RecentAppsGestureMonitor(
     private val gestureDistanceThresholdPx: Int,
-    override val gestureStateChangedCallback: (GestureState) -> Unit,
     private val velocityThresholdPxPerMs: Float,
-    private val velocityTracker: VelocityTracker1D = VelocityTracker1D(isDataDifferential = false),
+    private val distanceTracker: DistanceTracker = DistanceTracker(),
+    private val velocityTracker: VerticalVelocityTracker = VerticalVelocityTracker(),
 ) : TouchpadGestureMonitor {
 
-    private var xStart = 0f
-    private var yStart = 0f
+    private var gestureStateChangedCallback: (GestureState) -> Unit = {}
 
-    override fun processTouchpadEvent(event: MotionEvent) {
-        val action = event.actionMasked
-        velocityTracker.addDataPoint(event.eventTime, event.y)
-        when (action) {
-            MotionEvent.ACTION_DOWN -> {
-                if (isThreeFingerTouchpadSwipe(event)) {
-                    xStart = event.x
-                    yStart = event.y
-                    gestureStateChangedCallback(GestureState.InProgress())
-                }
-            }
-            MotionEvent.ACTION_UP -> {
-                if (isThreeFingerTouchpadSwipe(event) && isRecentAppsGesture(event)) {
-                    gestureStateChangedCallback(GestureState.Finished)
-                } else {
-                    gestureStateChangedCallback(GestureState.NotStarted)
-                }
-                velocityTracker.resetTracking()
-            }
-            MotionEvent.ACTION_CANCEL -> {
-                velocityTracker.resetTracking()
-            }
-        }
+    override fun addGestureStateCallback(callback: (GestureState) -> Unit) {
+        gestureStateChangedCallback = callback
     }
 
-    private fun isRecentAppsGesture(event: MotionEvent): Boolean {
-        // below is trying to mirror behavior of TriggerSwipeUpTouchTracker#onGestureEnd.
-        // We're diving velocity by 1000, to have the same unit of measure: pixels/ms.
-        val swipeDistance = yStart - event.y
-        val velocity = velocityTracker.calculateVelocity() / 1000
-        return swipeDistance >= gestureDistanceThresholdPx &&
-            abs(velocity) <= velocityThresholdPxPerMs
+    override fun accept(event: MotionEvent) {
+        if (!isThreeFingerTouchpadSwipe(event)) return
+        val gestureState = distanceTracker.processEvent(event)
+        velocityTracker.accept(event)
+
+        updateGestureState(
+            gestureStateChangedCallback,
+            gestureState,
+            isFinished = { state ->
+                -state.deltaY >= gestureDistanceThresholdPx &&
+                    abs(velocityTracker.calculateVelocity().value) <= velocityThresholdPxPerMs
+            },
+            progress = { 0f },
+        )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
index 88671d4..4b82ba1 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
@@ -18,13 +18,14 @@
 
 import android.view.InputDevice
 import android.view.MotionEvent
+import java.util.function.Consumer
 
 /**
  * Allows listening to touchpadGesture and calling onDone when gesture was triggered. Can have all
  * motion events passed to [onMotionEvent] and will filter touchpad events accordingly
  */
 class TouchpadGestureHandler(
-    private val gestureMonitor: TouchpadGestureMonitor,
+    private val gestureMonitor: Consumer<MotionEvent>,
     private val easterEggGestureMonitor: EasterEggGestureMonitor,
 ) {
 
@@ -40,7 +41,7 @@
             if (isTwoFingerSwipe(event)) {
                 easterEggGestureMonitor.processTouchpadEvent(event)
             } else {
-                gestureMonitor.processTouchpadEvent(event)
+                gestureMonitor.accept(event)
             }
             true
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt
index 4655c98..9216821 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt
@@ -17,15 +17,11 @@
 package com.android.systemui.touchpad.tutorial.ui.gesture
 
 import android.view.MotionEvent
+import java.util.function.Consumer
 
-/**
- * Monitor for touchpad gestures that calls [gestureStateChangedCallback] when [GestureState]
- * changes. All tracked motion events should be passed to [processTouchpadEvent]
- */
-interface TouchpadGestureMonitor {
-    val gestureStateChangedCallback: (GestureState) -> Unit
-
-    fun processTouchpadEvent(event: MotionEvent)
+/** Monitor for touchpad gestures that can notify callback when [GestureState] changes. */
+interface TouchpadGestureMonitor : Consumer<MotionEvent> {
+    fun addGestureStateCallback(callback: (GestureState) -> Unit)
 }
 
 fun isThreeFingerTouchpadSwipe(event: MotionEvent) = isNFingerTouchpadSwipe(event, fingerCount = 3)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt
new file mode 100644
index 0000000..9b38eca
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 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.touchpad.tutorial.ui.gesture
+
+import android.view.MotionEvent
+import androidx.compose.ui.input.pointer.util.VelocityTracker1D
+import java.util.function.Consumer
+
+/** Velocity in pixels/ms. */
+@JvmInline value class Velocity(val value: Float)
+
+/**
+ * Tracks velocity for processed MotionEvents. Useful for recognizing gestures based on velocity.
+ */
+interface VelocityTracker : Consumer<MotionEvent> {
+
+    fun calculateVelocity(): Velocity
+}
+
+class VerticalVelocityTracker(
+    private val velocityTracker: VelocityTracker1D = VelocityTracker1D(isDataDifferential = false)
+) : VelocityTracker {
+
+    override fun accept(event: MotionEvent) {
+        val action = event.actionMasked
+        if (action == MotionEvent.ACTION_DOWN) {
+            velocityTracker.resetTracking()
+        }
+        velocityTracker.addDataPoint(event.eventTime, event.y)
+    }
+
+    /**
+     * Calculates velocity on demand - this calculation can be expensive so shouldn't be called
+     * after every event.
+     */
+    override fun calculateVelocity() = Velocity(velocityTracker.calculateVelocity() / 1000)
+}