Merge "Handling case when touchpad gesture is in progress" into main
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 94ff65e..4e1829a 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
@@ -26,6 +26,7 @@
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
@@ -49,6 +50,7 @@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.LottieComposition
 import com.airbnb.lottie.LottieProperty
 import com.airbnb.lottie.compose.LottieAnimation
 import com.airbnb.lottie.compose.LottieCompositionSpec
@@ -61,6 +63,9 @@
 import com.airbnb.lottie.compose.rememberLottieDynamicProperty
 import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.systemui.res.R
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
 import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK
 import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler
 
@@ -78,23 +83,49 @@
 ) {
     val screenColors = rememberScreenColors()
     BackHandler(onBack = onBack)
-    var gestureDone by remember { mutableStateOf(false) }
+    var gestureState by remember { mutableStateOf(GestureState.NOT_STARTED) }
     val swipeDistanceThresholdPx =
         LocalContext.current.resources.getDimensionPixelSize(
             com.android.internal.R.dimen.system_gestures_distance_threshold
         )
     val gestureHandler =
         remember(swipeDistanceThresholdPx) {
-            TouchpadGestureHandler(BACK, swipeDistanceThresholdPx, onDone = { gestureDone = true })
+            TouchpadGestureHandler(
+                BACK,
+                swipeDistanceThresholdPx,
+                onGestureStateChanged = { gestureState = it }
+            )
         }
+    TouchpadGesturesHandlingBox(gestureHandler, gestureState) {
+        GestureTutorialContent(gestureState, onDoneButtonClicked, screenColors)
+    }
+}
+
+@Composable
+private fun TouchpadGesturesHandlingBox(
+    gestureHandler: TouchpadGestureHandler,
+    gestureState: GestureState,
+    modifier: Modifier = Modifier,
+    content: @Composable BoxScope.() -> Unit
+) {
     Box(
         modifier =
-            Modifier.fillMaxSize()
+            modifier
+                .fillMaxSize()
                 // we need to use pointerInteropFilter because some info about touchpad gestures is
                 // only available in MotionEvent
-                .pointerInteropFilter(onTouchEvent = gestureHandler::onMotionEvent)
+                .pointerInteropFilter(
+                    onTouchEvent = { event ->
+                        // FINISHED is the final state so we don't need to process touches anymore
+                        if (gestureState != FINISHED) {
+                            gestureHandler.onMotionEvent(event)
+                        } else {
+                            false
+                        }
+                    }
+                )
     ) {
-        GestureTutorialContent(gestureDone, onDoneButtonClicked, screenColors)
+        content()
     }
 }
 
@@ -126,14 +157,14 @@
 
 @Composable
 private fun GestureTutorialContent(
-    gestureDone: Boolean,
+    gestureState: GestureState,
     onDoneButtonClicked: () -> Unit,
     screenColors: TutorialScreenColors
 ) {
     val animatedColor by
         animateColorAsState(
             targetValue =
-                if (gestureDone) screenColors.successBackgroundColor
+                if (gestureState == FINISHED) screenColors.successBackgroundColor
                 else screenColors.backgroundColor,
             animationSpec = tween(durationMillis = 150, easing = LinearEasing),
             label = "backgroundColor"
@@ -148,7 +179,7 @@
         Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
             TutorialDescription(
                 titleTextId =
-                    if (gestureDone) R.string.touchpad_tutorial_gesture_done
+                    if (gestureState == FINISHED) R.string.touchpad_tutorial_gesture_done
                     else R.string.touchpad_back_gesture_action_title,
                 titleColor = screenColors.titleColor,
                 bodyTextId = R.string.touchpad_back_gesture_guidance,
@@ -156,7 +187,7 @@
             )
             Spacer(modifier = Modifier.width(76.dp))
             TutorialAnimation(
-                gestureDone,
+                gestureState,
                 screenColors.animationProperties,
                 modifier = Modifier.weight(1f).padding(top = 8.dp)
             )
@@ -189,27 +220,38 @@
 
 @Composable
 fun TutorialAnimation(
-    gestureDone: Boolean,
+    gestureState: GestureState,
     animationProperties: LottieDynamicProperties,
     modifier: Modifier = Modifier
 ) {
     Column(modifier = modifier.fillMaxWidth()) {
-        val resId = if (gestureDone) R.raw.trackpad_back_success else R.raw.trackpad_back_edu
+        val resId =
+            if (gestureState == FINISHED) R.raw.trackpad_back_success else R.raw.trackpad_back_edu
         val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(resId))
-        val progress by
-            animateLottieCompositionAsState(
-                composition,
-                iterations = if (gestureDone) 1 else LottieConstants.IterateForever
-            )
+        val progress = progressForGestureState(composition, gestureState)
         LottieAnimation(
             composition = composition,
-            progress = { progress },
+            progress = progress,
             dynamicProperties = animationProperties
         )
     }
 }
 
 @Composable
+private fun progressForGestureState(
+    composition: LottieComposition?,
+    gestureState: GestureState
+): () -> Float {
+    if (gestureState == IN_PROGRESS) {
+        return { 0f } // when gesture is in progress, animation should freeze on 1st frame
+    } else {
+        val iterations = if (gestureState == FINISHED) 1 else LottieConstants.IterateForever
+        val animationState by animateLottieCompositionAsState(composition, iterations = iterations)
+        return { animationState }
+    }
+}
+
+@Composable
 fun rememberColorFilterProperty(
     layerName: String,
     color: Color
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 1fa7a0c..6fa9bcd 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
@@ -17,23 +17,26 @@
 package com.android.systemui.touchpad.tutorial.ui.gesture
 
 import android.view.MotionEvent
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
 import kotlin.math.abs
 
 /**
- * Monitor for touchpad gestures that calls [gestureDoneCallback] when gesture was successfully
- * done. All tracked motion events should be passed to [processTouchpadEvent]
+ * Monitor for touchpad gestures that calls [gestureStateChangedCallback] when [GestureState]
+ * changes. All tracked motion events should be passed to [processTouchpadEvent]
  */
 interface TouchpadGestureMonitor {
 
     val gestureDistanceThresholdPx: Int
-    val gestureDoneCallback: () -> Unit
+    val gestureStateChangedCallback: (GestureState) -> Unit
 
     fun processTouchpadEvent(event: MotionEvent)
 }
 
 class BackGestureMonitor(
     override val gestureDistanceThresholdPx: Int,
-    override val gestureDoneCallback: () -> Unit
+    override val gestureStateChangedCallback: (GestureState) -> Unit
 ) : TouchpadGestureMonitor {
 
     private var xStart = 0f
@@ -44,13 +47,16 @@
             MotionEvent.ACTION_DOWN -> {
                 if (isThreeFingerTouchpadSwipe(event)) {
                     xStart = event.x
+                    gestureStateChangedCallback(IN_PROGRESS)
                 }
             }
             MotionEvent.ACTION_UP -> {
                 if (isThreeFingerTouchpadSwipe(event)) {
                     val distance = abs(event.x - xStart)
                     if (distance >= gestureDistanceThresholdPx) {
-                        gestureDoneCallback()
+                        gestureStateChangedCallback(FINISHED)
+                    } else {
+                        gestureStateChangedCallback(NOT_STARTED)
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
new file mode 100644
index 0000000..446875a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
@@ -0,0 +1,23 @@
+/*
+ * 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
+
+enum class GestureState {
+    NOT_STARTED,
+    IN_PROGRESS,
+    FINISHED
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
index 4ae9c7b..190da62 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
@@ -22,10 +22,10 @@
 
     fun toMonitor(
         swipeDistanceThresholdPx: Int,
-        gestureDoneCallback: () -> Unit
+        onStateChanged: (GestureState) -> Unit
     ): TouchpadGestureMonitor {
         return when (this) {
-            BACK -> BackGestureMonitor(swipeDistanceThresholdPx, gestureDoneCallback)
+            BACK -> BackGestureMonitor(swipeDistanceThresholdPx, onStateChanged)
             else -> throw IllegalArgumentException("Not implemented yet")
         }
     }
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 dc8471c..cac2a99 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
@@ -26,11 +26,11 @@
 class TouchpadGestureHandler(
     touchpadGesture: TouchpadGesture,
     swipeDistanceThresholdPx: Int,
-    onDone: () -> Unit
+    onGestureStateChanged: (GestureState) -> Unit
 ) {
 
     private val gestureRecognition =
-        touchpadGesture.toMonitor(swipeDistanceThresholdPx, gestureDoneCallback = onDone)
+        touchpadGesture.toMonitor(swipeDistanceThresholdPx, onStateChanged = onGestureStateChanged)
 
     fun onMotionEvent(event: MotionEvent): Boolean {
         // events from touchpad have SOURCE_MOUSE and not SOURCE_TOUCHPAD because of legacy reasons
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
index cf0db7b..8875b84 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
@@ -28,6 +28,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -36,16 +39,19 @@
 @RunWith(AndroidJUnit4::class)
 class BackGestureMonitorTest : SysuiTestCase() {
 
-    private var gestureDoneWasCalled = false
-    private val gestureDoneCallback = { gestureDoneWasCalled = true }
-    private val gestureMonitor = BackGestureMonitor(SWIPE_DISTANCE.toInt(), gestureDoneCallback)
+    private var gestureState = NOT_STARTED
+    private val gestureMonitor =
+        BackGestureMonitor(
+            gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
+            gestureStateChangedCallback = { gestureState = it }
+        )
 
     companion object {
         const val SWIPE_DISTANCE = 100f
     }
 
     @Test
-    fun triggersGestureDoneForThreeFingerGestureRight() {
+    fun triggersGestureFinishedForThreeFingerGestureRight() {
         val events =
             listOf(
                 threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f),
@@ -59,11 +65,11 @@
 
         events.forEach { gestureMonitor.processTouchpadEvent(it) }
 
-        assertThat(gestureDoneWasCalled).isTrue()
+        assertThat(gestureState).isEqualTo(FINISHED)
     }
 
     @Test
-    fun triggersGestureDoneForThreeFingerGestureLeft() {
+    fun triggersGestureFinishedForThreeFingerGestureLeft() {
         val events =
             listOf(
                 threeFingerEvent(ACTION_DOWN, x = SWIPE_DISTANCE, y = 0f),
@@ -77,7 +83,21 @@
 
         events.forEach { gestureMonitor.processTouchpadEvent(it) }
 
-        assertThat(gestureDoneWasCalled).isTrue()
+        assertThat(gestureState).isEqualTo(FINISHED)
+    }
+
+    @Test
+    fun triggersGestureProgressForThreeFingerGestureStarted() {
+        val events =
+            listOf(
+                threeFingerEvent(ACTION_DOWN, x = SWIPE_DISTANCE, y = 0f),
+                threeFingerEvent(ACTION_POINTER_DOWN, x = SWIPE_DISTANCE, y = 0f),
+                threeFingerEvent(ACTION_POINTER_DOWN, x = SWIPE_DISTANCE, y = 0f),
+            )
+
+        events.forEach { gestureMonitor.processTouchpadEvent(it) }
+
+        assertThat(gestureState).isEqualTo(IN_PROGRESS)
     }
 
     private fun threeFingerEvent(action: Int, x: Float, y: Float): MotionEvent {
@@ -91,7 +111,7 @@
     }
 
     @Test
-    fun doesntTriggerGestureDone_onThreeFingersSwipeUp() {
+    fun doesntTriggerGestureFinished_onThreeFingersSwipeUp() {
         val events =
             listOf(
                 threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f),
@@ -105,11 +125,11 @@
 
         events.forEach { gestureMonitor.processTouchpadEvent(it) }
 
-        assertThat(gestureDoneWasCalled).isFalse()
+        assertThat(gestureState).isEqualTo(NOT_STARTED)
     }
 
     @Test
-    fun doesntTriggerGestureDone_onTwoFingersSwipe() {
+    fun doesntTriggerGestureFinished_onTwoFingersSwipe() {
         fun twoFingerEvent(action: Int, x: Float, y: Float) =
             motionEvent(
                 action = action,
@@ -127,11 +147,11 @@
 
         events.forEach { gestureMonitor.processTouchpadEvent(it) }
 
-        assertThat(gestureDoneWasCalled).isFalse()
+        assertThat(gestureState).isEqualTo(NOT_STARTED)
     }
 
     @Test
-    fun doesntTriggerGestureDone_onFourFingersSwipe() {
+    fun doesntTriggerGestureFinished_onFourFingersSwipe() {
         fun fourFingerEvent(action: Int, x: Float, y: Float) =
             motionEvent(
                 action = action,
@@ -155,6 +175,6 @@
 
         events.forEach { gestureMonitor.processTouchpadEvent(it) }
 
-        assertThat(gestureDoneWasCalled).isFalse()
+        assertThat(gestureState).isEqualTo(NOT_STARTED)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
index 769f264..dc4d5f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
@@ -32,6 +32,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
 import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -41,8 +43,8 @@
 @RunWith(AndroidJUnit4::class)
 class TouchpadGestureHandlerTest : SysuiTestCase() {
 
-    private var gestureDone = false
-    private val handler = TouchpadGestureHandler(BACK, SWIPE_DISTANCE) { gestureDone = true }
+    private var gestureState = NOT_STARTED
+    private val handler = TouchpadGestureHandler(BACK, SWIPE_DISTANCE) { gestureState = it }
 
     companion object {
         const val SWIPE_DISTANCE = 100
@@ -84,7 +86,7 @@
     fun triggersGestureDoneForThreeFingerGesture() {
         backGestureEvents().forEach { handler.onMotionEvent(it) }
 
-        assertThat(gestureDone).isTrue()
+        assertThat(gestureState).isEqualTo(FINISHED)
     }
 
     private fun backGestureEvents(): List<MotionEvent> {