summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Michal Brzezinski <brzezinski@google.com> 2024-06-28 15:13:07 +0200
committer Michal Brzezinski <brzezinski@google.com> 2024-07-04 18:48:50 +0100
commit1b95862256b69daf0bf22fdb3730a6be690bd4f0 (patch)
tree93a48ba0a8ec1d65155197778ca9c9cd37d7d3dd
parent6f9beb9c6a98f3e7066deea8d327dcfacac1b0a7 (diff)
Handling back gesture in BackGestureTutorialScreen
The only thing that's reacting to the gesture now is title text changing to "Done" - registering MotionEvents handler to pass to ViewModel - GestureTutorialViewModel filters events coming from touchpad - BackGestureMonitor receives touchpad events and contains logic for recognizing the gesture Also introducing FakeMotionEvent methods that will be helpful for testing MotionEvents Bug: 346579074 Flag: com.android.systemui.shared.new_touchpad_gestures_tutorial Test: manually, see video Test: BackGestureMonitorTest Test: GestureTutorialViewModelTest Change-Id: I516c6d661e5d332fcccc78c21580095ddf90f4a7
-rw-r--r--packages/SystemUI/res/values/strings.xml2
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/GestureViewModelFactory.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt64
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt50
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/BackGestureTutorialScreen.kt66
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt15
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt160
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt66
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt124
10 files changed, 555 insertions, 63 deletions
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 82dafc3ed1df..5d568b78bcdc 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3577,6 +3577,8 @@
<string name="touchpad_tutorial_action_key_button">Action key</string>
<!-- Label for button finishing touchpad tutorial [CHAR LIMIT=NONE] -->
<string name="touchpad_tutorial_done_button">Done</string>
+ <!-- Screen title after gesture was done successfully [CHAR LIMIT=NONE] -->
+ <string name="touchpad_tutorial_gesture_done">Great job!</string>
<!-- Touchpad back gesture action name in tutorial [CHAR LIMIT=NONE] -->
<string name="touchpad_back_gesture_action_title">Go back</string>
<!-- Touchpad back gesture guidance in gestures tutorial [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/GestureViewModelFactory.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/GestureViewModelFactory.kt
deleted file mode 100644
index 504bd5fbcb42..000000000000
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/GestureViewModelFactory.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
-
-sealed class GestureTutorialViewModel : ViewModel()
-
-class BackGestureTutorialViewModel : GestureTutorialViewModel()
-
-class HomeGestureTutorialViewModel : GestureTutorialViewModel()
-
-class GestureViewModelFactory : ViewModelProvider.Factory {
-
- @Suppress("UNCHECKED_CAST")
- override fun <T : ViewModel> create(modelClass: Class<T>): T {
- return when (modelClass) {
- BackGestureTutorialViewModel::class.java -> BackGestureTutorialViewModel()
- HomeGestureTutorialViewModel::class.java -> HomeGestureTutorialViewModel()
- else -> error("Unknown ViewModel class: ${modelClass.name}")
- }
- as T
- }
-}
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
new file mode 100644
index 000000000000..1fa7a0c44171
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
@@ -0,0 +1,64 @@
+/*
+ * 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 kotlin.math.abs
+
+/**
+ * Monitor for touchpad gestures that calls [gestureDoneCallback] when gesture was successfully
+ * done. All tracked motion events should be passed to [processTouchpadEvent]
+ */
+interface TouchpadGestureMonitor {
+
+ val gestureDistanceThresholdPx: Int
+ val gestureDoneCallback: () -> Unit
+
+ fun processTouchpadEvent(event: MotionEvent)
+}
+
+class BackGestureMonitor(
+ override val gestureDistanceThresholdPx: Int,
+ override val gestureDoneCallback: () -> Unit
+) : TouchpadGestureMonitor {
+
+ private var xStart = 0f
+
+ override fun processTouchpadEvent(event: MotionEvent) {
+ val action = event.actionMasked
+ when (action) {
+ MotionEvent.ACTION_DOWN -> {
+ if (isThreeFingerTouchpadSwipe(event)) {
+ xStart = event.x
+ }
+ }
+ MotionEvent.ACTION_UP -> {
+ if (isThreeFingerTouchpadSwipe(event)) {
+ val distance = abs(event.x - xStart)
+ if (distance >= gestureDistanceThresholdPx) {
+ gestureDoneCallback()
+ }
+ }
+ }
+ }
+ }
+
+ private fun isThreeFingerTouchpadSwipe(event: MotionEvent): Boolean {
+ return event.classification == MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE &&
+ event.getAxisValue(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT) == 3f
+ }
+}
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
new file mode 100644
index 000000000000..4ae9c7b2426c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
@@ -0,0 +1,32 @@
+/*
+ * 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 TouchpadGesture {
+ BACK,
+ HOME;
+
+ fun toMonitor(
+ swipeDistanceThresholdPx: Int,
+ gestureDoneCallback: () -> Unit
+ ): TouchpadGestureMonitor {
+ return when (this) {
+ BACK -> BackGestureMonitor(swipeDistanceThresholdPx, gestureDoneCallback)
+ 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
new file mode 100644
index 000000000000..dc8471c3248a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.InputDevice
+import android.view.MotionEvent
+
+/**
+ * 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(
+ touchpadGesture: TouchpadGesture,
+ swipeDistanceThresholdPx: Int,
+ onDone: () -> Unit
+) {
+
+ private val gestureRecognition =
+ touchpadGesture.toMonitor(swipeDistanceThresholdPx, gestureDoneCallback = onDone)
+
+ fun onMotionEvent(event: MotionEvent): Boolean {
+ // events from touchpad have SOURCE_MOUSE and not SOURCE_TOUCHPAD because of legacy reasons
+ val isFromTouchpad =
+ event.isFromSource(InputDevice.SOURCE_MOUSE) &&
+ event.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER
+ val buttonClick =
+ event.actionMasked == MotionEvent.ACTION_DOWN &&
+ event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
+ return if (isFromTouchpad && !buttonClick) {
+ gestureRecognition.processTouchpadEvent(event)
+ true
+ } else {
+ false
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/BackGestureTutorialScreen.kt
index 2460761c8592..04a47985dc29 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/BackGestureTutorialScreen.kt
@@ -17,9 +17,11 @@
package com.android.systemui.touchpad.tutorial.ui.view
import androidx.activity.compose.BackHandler
+import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -31,29 +33,67 @@ import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInteropFilter
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.android.systemui.res.R
+import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK
+import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler
+@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun BackGestureTutorialScreen(
onDoneButtonClicked: () -> Unit,
onBack: () -> Unit,
- modifier: Modifier = Modifier,
) {
- BackHandler { onBack() }
+ BackHandler(onBack = onBack)
+ var gestureDone by remember { mutableStateOf(false) }
+ val swipeDistanceThresholdPx =
+ with(LocalContext.current) {
+ resources.getDimensionPixelSize(
+ com.android.internal.R.dimen.system_gestures_distance_threshold
+ )
+ }
+ val gestureHandler =
+ remember(swipeDistanceThresholdPx) {
+ TouchpadGestureHandler(BACK, swipeDistanceThresholdPx, onDone = { gestureDone = true })
+ }
+ Box(
+ modifier =
+ Modifier.fillMaxSize()
+ // we need to use pointerInteropFilter because some info about touchpad gestures is
+ // only available in MotionEvent
+ .pointerInteropFilter(onTouchEvent = gestureHandler::onMotionEvent)
+ ) {
+ GestureTutorialContent(gestureDone, onDoneButtonClicked)
+ }
+}
+
+@Composable
+private fun GestureTutorialContent(gestureDone: Boolean, onDoneButtonClicked: () -> Unit) {
Column(
verticalArrangement = Arrangement.Center,
modifier =
- modifier
+ Modifier.fillMaxSize()
.background(color = MaterialTheme.colorScheme.surfaceContainer)
.padding(start = 48.dp, top = 124.dp, end = 48.dp, bottom = 48.dp)
- .fillMaxSize()
) {
Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
- TutorialDescription(modifier = Modifier.weight(1f))
+ TutorialDescription(
+ titleTextId =
+ if (gestureDone) R.string.touchpad_tutorial_gesture_done
+ else R.string.touchpad_back_gesture_action_title,
+ bodyTextId = R.string.touchpad_back_gesture_guidance,
+ modifier = Modifier.weight(1f)
+ )
Spacer(modifier = Modifier.width(76.dp))
TutorialAnimation(modifier = Modifier.weight(1f).padding(top = 24.dp))
}
@@ -62,17 +102,15 @@ fun BackGestureTutorialScreen(
}
@Composable
-fun TutorialDescription(modifier: Modifier = Modifier) {
+fun TutorialDescription(
+ @StringRes titleTextId: Int,
+ @StringRes bodyTextId: Int,
+ modifier: Modifier = Modifier
+) {
Column(verticalArrangement = Arrangement.Top, modifier = modifier) {
- Text(
- text = stringResource(id = R.string.touchpad_back_gesture_action_title),
- style = MaterialTheme.typography.displayLarge
- )
+ Text(text = stringResource(id = titleTextId), style = MaterialTheme.typography.displayLarge)
Spacer(modifier = Modifier.height(16.dp))
- Text(
- text = stringResource(id = R.string.touchpad_back_gesture_guidance),
- style = MaterialTheme.typography.bodyLarge
- )
+ Text(text = stringResource(id = bodyTextId), style = MaterialTheme.typography.bodyLarge)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
index 0c5c1874db8a..93c26583e11e 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
@@ -17,6 +17,7 @@
package com.android.systemui.touchpad.tutorial.ui.view
import android.os.Bundle
+import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
@@ -25,10 +26,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.Lifecycle.State.STARTED
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.compose.theme.PlatformTheme
-import com.android.systemui.touchpad.tutorial.ui.GestureViewModelFactory
-import com.android.systemui.touchpad.tutorial.ui.HomeGestureTutorialViewModel
import com.android.systemui.touchpad.tutorial.ui.Screen.BACK_GESTURE
import com.android.systemui.touchpad.tutorial.ui.Screen.HOME_GESTURE
import com.android.systemui.touchpad.tutorial.ui.Screen.TUTORIAL_SELECTION
@@ -47,6 +45,8 @@ constructor(
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent { PlatformTheme { TouchpadTutorialScreen(vm) { finish() } } }
+ // required to handle 3+ fingers on touchpad
+ window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
}
override fun onResume() {
@@ -74,13 +74,8 @@ fun TouchpadTutorialScreen(vm: TouchpadTutorialViewModel, closeTutorial: () -> U
BACK_GESTURE ->
BackGestureTutorialScreen(
onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) },
- onBack = { vm.goTo(TUTORIAL_SELECTION) }
+ onBack = { vm.goTo(TUTORIAL_SELECTION) },
)
- HOME_GESTURE -> HomeGestureTutorialScreen()
+ HOME_GESTURE -> {}
}
}
-
-@Composable
-fun HomeGestureTutorialScreen() {
- val vm = viewModel<HomeGestureTutorialViewModel>(factory = GestureViewModelFactory())
-}
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
new file mode 100644
index 000000000000..cf0db7b51676
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
@@ -0,0 +1,160 @@
+/*
+ * 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 android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_POINTER_DOWN
+import android.view.MotionEvent.ACTION_POINTER_UP
+import android.view.MotionEvent.ACTION_UP
+import android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT
+import android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE
+import android.view.MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BackGestureMonitorTest : SysuiTestCase() {
+
+ private var gestureDoneWasCalled = false
+ private val gestureDoneCallback = { gestureDoneWasCalled = true }
+ private val gestureMonitor = BackGestureMonitor(SWIPE_DISTANCE.toInt(), gestureDoneCallback)
+
+ companion object {
+ const val SWIPE_DISTANCE = 100f
+ }
+
+ @Test
+ fun triggersGestureDoneForThreeFingerGestureRight() {
+ val events =
+ listOf(
+ threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f),
+ threeFingerEvent(ACTION_POINTER_DOWN, x = 0f, y = 0f),
+ threeFingerEvent(ACTION_POINTER_DOWN, x = 0f, y = 0f),
+ threeFingerEvent(ACTION_MOVE, x = SWIPE_DISTANCE / 2, y = 0f),
+ threeFingerEvent(ACTION_POINTER_UP, x = SWIPE_DISTANCE, y = 0f),
+ threeFingerEvent(ACTION_POINTER_UP, x = SWIPE_DISTANCE, y = 0f),
+ threeFingerEvent(ACTION_UP, x = SWIPE_DISTANCE, y = 0f),
+ )
+
+ events.forEach { gestureMonitor.processTouchpadEvent(it) }
+
+ assertThat(gestureDoneWasCalled).isTrue()
+ }
+
+ @Test
+ fun triggersGestureDoneForThreeFingerGestureLeft() {
+ 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),
+ threeFingerEvent(ACTION_MOVE, x = SWIPE_DISTANCE / 2, y = 0f),
+ threeFingerEvent(ACTION_POINTER_UP, x = 0f, y = 0f),
+ threeFingerEvent(ACTION_POINTER_UP, x = 0f, y = 0f),
+ threeFingerEvent(ACTION_UP, x = 0f, y = 0f),
+ )
+
+ events.forEach { gestureMonitor.processTouchpadEvent(it) }
+
+ assertThat(gestureDoneWasCalled).isTrue()
+ }
+
+ private fun threeFingerEvent(action: Int, x: Float, y: Float): MotionEvent {
+ return motionEvent(
+ action = action,
+ x = x,
+ y = y,
+ classification = CLASSIFICATION_MULTI_FINGER_SWIPE,
+ axisValues = mapOf(AXIS_GESTURE_SWIPE_FINGER_COUNT to 3f)
+ )
+ }
+
+ @Test
+ fun doesntTriggerGestureDone_onThreeFingersSwipeUp() {
+ val events =
+ listOf(
+ threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f),
+ threeFingerEvent(ACTION_POINTER_DOWN, x = 0f, y = 0f),
+ threeFingerEvent(ACTION_POINTER_DOWN, x = 0f, y = 0f),
+ threeFingerEvent(ACTION_MOVE, x = 0f, y = SWIPE_DISTANCE / 2),
+ threeFingerEvent(ACTION_POINTER_UP, x = 0f, y = SWIPE_DISTANCE),
+ threeFingerEvent(ACTION_POINTER_UP, x = 0f, y = SWIPE_DISTANCE),
+ threeFingerEvent(ACTION_UP, x = 0f, y = SWIPE_DISTANCE),
+ )
+
+ events.forEach { gestureMonitor.processTouchpadEvent(it) }
+
+ assertThat(gestureDoneWasCalled).isFalse()
+ }
+
+ @Test
+ fun doesntTriggerGestureDone_onTwoFingersSwipe() {
+ fun twoFingerEvent(action: Int, x: Float, y: Float) =
+ motionEvent(
+ action = action,
+ x = x,
+ y = y,
+ classification = CLASSIFICATION_TWO_FINGER_SWIPE,
+ axisValues = mapOf(AXIS_GESTURE_SWIPE_FINGER_COUNT to 2f)
+ )
+ val events =
+ listOf(
+ twoFingerEvent(ACTION_DOWN, x = 0f, y = 0f),
+ twoFingerEvent(ACTION_MOVE, x = SWIPE_DISTANCE / 2, y = 0f),
+ twoFingerEvent(ACTION_UP, x = SWIPE_DISTANCE, y = 0f),
+ )
+
+ events.forEach { gestureMonitor.processTouchpadEvent(it) }
+
+ assertThat(gestureDoneWasCalled).isFalse()
+ }
+
+ @Test
+ fun doesntTriggerGestureDone_onFourFingersSwipe() {
+ fun fourFingerEvent(action: Int, x: Float, y: Float) =
+ motionEvent(
+ action = action,
+ x = x,
+ y = y,
+ classification = CLASSIFICATION_MULTI_FINGER_SWIPE,
+ axisValues = mapOf(AXIS_GESTURE_SWIPE_FINGER_COUNT to 4f)
+ )
+ val events =
+ listOf(
+ fourFingerEvent(ACTION_DOWN, x = 0f, y = 0f),
+ fourFingerEvent(ACTION_POINTER_DOWN, x = 0f, y = 0f),
+ fourFingerEvent(ACTION_POINTER_DOWN, x = 0f, y = 0f),
+ fourFingerEvent(ACTION_POINTER_DOWN, x = 0f, y = 0f),
+ fourFingerEvent(ACTION_MOVE, x = SWIPE_DISTANCE / 2, y = 0f),
+ fourFingerEvent(ACTION_POINTER_UP, x = SWIPE_DISTANCE, y = 0f),
+ fourFingerEvent(ACTION_POINTER_UP, x = SWIPE_DISTANCE, y = 0f),
+ fourFingerEvent(ACTION_POINTER_UP, x = SWIPE_DISTANCE, y = 0f),
+ fourFingerEvent(ACTION_UP, x = SWIPE_DISTANCE, y = 0f),
+ )
+
+ events.forEach { gestureMonitor.processTouchpadEvent(it) }
+
+ assertThat(gestureDoneWasCalled).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt
new file mode 100644
index 000000000000..e632e3474b96
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.InputDevice.SOURCE_CLASS_POINTER
+import android.view.InputDevice.SOURCE_MOUSE
+import android.view.MotionEvent
+import android.view.MotionEvent.CLASSIFICATION_NONE
+import android.view.MotionEvent.TOOL_TYPE_FINGER
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.spy
+
+fun motionEvent(
+ action: Int,
+ x: Float,
+ y: Float,
+ source: Int = 0,
+ toolType: Int = TOOL_TYPE_FINGER,
+ pointerCount: Int = 1,
+ axisValues: Map<Int, Float> = emptyMap(),
+ classification: Int = CLASSIFICATION_NONE,
+): MotionEvent {
+ val event =
+ MotionEvent.obtain(/* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0)
+ event.source = source
+ return spy<MotionEvent>(event) {
+ on { getToolType(0) } doReturn toolType
+ on { getPointerCount() } doReturn pointerCount
+ axisValues.forEach { (key, value) -> on { getAxisValue(key) } doReturn value }
+ on { getClassification() } doReturn classification
+ }
+}
+
+fun touchpadEvent(
+ action: Int,
+ x: Float,
+ y: Float,
+ pointerCount: Int = 1,
+ classification: Int = CLASSIFICATION_NONE,
+ axisValues: Map<Int, Float> = emptyMap()
+): MotionEvent {
+ return motionEvent(
+ action = action,
+ x = x,
+ y = y,
+ source = SOURCE_MOUSE or SOURCE_CLASS_POINTER,
+ toolType = TOOL_TYPE_FINGER,
+ pointerCount = pointerCount,
+ classification = classification,
+ axisValues = axisValues
+ )
+}
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
new file mode 100644
index 000000000000..769f264f0870
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.InputDevice.SOURCE_MOUSE
+import android.view.InputDevice.SOURCE_TOUCHSCREEN
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_HOVER_ENTER
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_POINTER_DOWN
+import android.view.MotionEvent.ACTION_POINTER_UP
+import android.view.MotionEvent.ACTION_UP
+import android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT
+import android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE
+import android.view.MotionEvent.TOOL_TYPE_FINGER
+import android.view.MotionEvent.TOOL_TYPE_MOUSE
+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.TouchpadGesture.BACK
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TouchpadGestureHandlerTest : SysuiTestCase() {
+
+ private var gestureDone = false
+ private val handler = TouchpadGestureHandler(BACK, SWIPE_DISTANCE) { gestureDone = true }
+
+ companion object {
+ const val SWIPE_DISTANCE = 100
+ }
+
+ @Test
+ fun handlesEventsFromTouchpad() {
+ val event = downEvent(source = SOURCE_MOUSE, toolType = TOOL_TYPE_FINGER)
+ val eventHandled = handler.onMotionEvent(event)
+ assertThat(eventHandled).isTrue()
+ }
+
+ @Test
+ fun ignoresEventsFromMouse() {
+ val event = downEvent(source = SOURCE_MOUSE, toolType = TOOL_TYPE_MOUSE)
+ val eventHandled = handler.onMotionEvent(event)
+ assertThat(eventHandled).isFalse()
+ }
+
+ @Test
+ fun ignoresEventsFromTouch() {
+ val event = downEvent(source = SOURCE_TOUCHSCREEN, toolType = TOOL_TYPE_FINGER)
+ val eventHandled = handler.onMotionEvent(event)
+ assertThat(eventHandled).isFalse()
+ }
+
+ @Test
+ fun ignoresButtonClicksFromTouchpad() {
+ val event = downEvent(source = SOURCE_MOUSE, toolType = TOOL_TYPE_FINGER)
+ event.buttonState = MotionEvent.BUTTON_PRIMARY
+ val eventHandled = handler.onMotionEvent(event)
+ assertThat(eventHandled).isFalse()
+ }
+
+ private fun downEvent(source: Int, toolType: Int) =
+ motionEvent(action = ACTION_DOWN, x = 0f, y = 0f, source = source, toolType = toolType)
+
+ @Test
+ fun triggersGestureDoneForThreeFingerGesture() {
+ backGestureEvents().forEach { handler.onMotionEvent(it) }
+
+ assertThat(gestureDone).isTrue()
+ }
+
+ private fun backGestureEvents(): List<MotionEvent> {
+ // list of motion events read from device while doing back gesture
+ val y = 100f
+ return listOf(
+ touchpadEvent(ACTION_HOVER_ENTER, x = 759f, y = y, pointerCount = 1),
+ threeFingerTouchpadEvent(ACTION_DOWN, x = 759f, y = y, pointerCount = 1),
+ threeFingerTouchpadEvent(ACTION_POINTER_DOWN, x = 759f, y = y, pointerCount = 2),
+ threeFingerTouchpadEvent(ACTION_POINTER_DOWN, x = 759f, y = y, pointerCount = 3),
+ threeFingerTouchpadEvent(ACTION_MOVE, x = 767f, y = y, pointerCount = 3),
+ threeFingerTouchpadEvent(ACTION_MOVE, x = 785f, y = y, pointerCount = 3),
+ threeFingerTouchpadEvent(ACTION_MOVE, x = 814f, y = y, pointerCount = 3),
+ threeFingerTouchpadEvent(ACTION_MOVE, x = 848f, y = y, pointerCount = 3),
+ threeFingerTouchpadEvent(ACTION_MOVE, x = 943f, y = y, pointerCount = 3),
+ threeFingerTouchpadEvent(ACTION_POINTER_UP, x = 943f, y = y, pointerCount = 3),
+ threeFingerTouchpadEvent(ACTION_POINTER_UP, x = 943f, y = y, pointerCount = 2),
+ threeFingerTouchpadEvent(ACTION_UP, x = 943f, y = y, pointerCount = 1)
+ )
+ }
+
+ private fun threeFingerTouchpadEvent(
+ action: Int,
+ x: Float,
+ y: Float,
+ pointerCount: Int
+ ): MotionEvent {
+ return touchpadEvent(
+ action = action,
+ x = x,
+ y = y,
+ pointerCount = pointerCount,
+ classification = CLASSIFICATION_MULTI_FINGER_SWIPE,
+ axisValues = mapOf(AXIS_GESTURE_SWIPE_FINGER_COUNT to 3f)
+ )
+ }
+}